├── .babelrc ├── .eslintrc ├── .fecsrc ├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── README.zh-CN.md ├── bin └── csshint-cli ├── package.json ├── src ├── checker.js ├── cli.js ├── colors.js ├── config.js ├── csshint.yml ├── prefixes.js ├── rule │ ├── adjoining-classes.js │ ├── always-semicolon.js │ ├── block-indent.js │ ├── box-model.js │ ├── box-sizing.js │ ├── bulletproof-font-face.js │ ├── disallow-expression.js │ ├── disallow-important.js │ ├── disallow-named-color.js │ ├── disallow-overqualified-elements.js │ ├── disallow-quotes-in-url.js │ ├── display-property-grouping.js │ ├── duplicate-background-images.js │ ├── duplicate-properties.js │ ├── empty-rules.js │ ├── fallback-colors.js │ ├── floats.js │ ├── font-face.js │ ├── font-sizes.js │ ├── gradients.js │ ├── hex-color.js │ ├── horizontal-vertical-position.js │ ├── ids.js │ ├── import.js │ ├── leading-zero.js │ ├── max-length.js │ ├── max-selector-nesting-level.js │ ├── min-font-size.js │ ├── no-bom.js │ ├── omit-protocol-in-url.js │ ├── outline-none.js │ ├── property-not-existed.js │ ├── qualified-headings.js │ ├── regex-selectors.js │ ├── require-after-space.js │ ├── require-around-space.js │ ├── require-before-space.js │ ├── require-doublequotes.js │ ├── require-newline.js │ ├── require-number.js │ ├── require-transition-property.js │ ├── shorthand.js │ ├── star-property-hack.js │ ├── text-indent.js │ ├── underscore-property-hack.js │ ├── unifying-color-case-sensitive.js │ ├── unifying-font-family-case-sensitive.js │ ├── unique-headings.js │ ├── universal-selector.js │ ├── unqualified-attributes.js │ ├── vendor-prefixes-sort.js │ └── zero-unit.js └── util.js └── test ├── fixture ├── .csshintignore ├── .csshintrc ├── .csshintrc.yml ├── adjoining-classes.css ├── always-semicolon.css ├── always-semicolon2.css ├── block-indent-new1.css ├── block-indent-new2.css ├── block-indent.css ├── block-indent1.css ├── block-indent2.css ├── block-indent3.css ├── block-indent4.css ├── block-indent5.css ├── block-indent6.css ├── block-indent7.css ├── block-indent8.css ├── box-model.css ├── box-sizing.css ├── bulletproof-font-face.css ├── code-test.css ├── cr.css ├── cr1.css ├── csshintignore.css ├── disallow-expression.css ├── disallow-important.css ├── disallow-named-color.css ├── disallow-overqualified-elements.css ├── disallow-overqualified-elements1.css ├── disallow-quotes-in-url.css ├── display-property-grouping.css ├── duplicate-background-images.css ├── duplicate-properties.css ├── empty-rules.css ├── error-char.css ├── fallback-colors.css ├── floats.css ├── font-face.css ├── font-sizes.css ├── gradients.css ├── has-bom.css ├── hex-color.css ├── horizontal-vertical-position.css ├── ids.css ├── import.css ├── index.css ├── inline-comment.css ├── leading-zero.css ├── max-length.css ├── max-length2.css ├── max-selector-nesting-level.css ├── min-font-size.css ├── no-bom.css ├── omit-protocol-in-url.css ├── outline-none.css ├── page.css ├── photo2.css ├── property-not-existed.css ├── qualified-headings.css ├── regex-selectors.css ├── require-after-space.css ├── require-after-space2.css ├── require-around-space.css ├── require-before-space.css ├── require-doublequotes.css ├── require-newline.css ├── require-number.css ├── require-transition-property.css ├── reset.css ├── rss.css ├── sendWrap.css ├── shorthand.css ├── star-property-hack.css ├── test.css ├── text-indent.css ├── topic.css ├── underscore-property-hack.css ├── unicode.css ├── unifying-color-case-sensitive.css ├── unifying-font-family-case-sensitive.css ├── unifying-font-family-case-sensitive1.css ├── unique-headings.css ├── universal-selector.css ├── unqualified-attributes.css ├── vendor-prefixes-sort.css └── zero-unit.css └── spec ├── checker.spec.js ├── prefixes.spec.js ├── rule.spec.js └── util.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "rules": { 6 | "no-console": 0 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 7, 10 | "sourceType": "module", 11 | "ecmaFeatures": { 12 | "jsx": true 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.fecsrc: -------------------------------------------------------------------------------- 1 | files: 2 | - src 3 | eslint: 4 | env: 5 | es6: true 6 | rules: 7 | no-console: 0 8 | fecs-camelcase: 9 | - 2 10 | - 11 | ignore: 12 | - "/-_/" 13 | fecs-min-vars-per-destructure: false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | npm-debug.log 4 | Thumbs.db 5 | .DS_Store 6 | *.swp 7 | *bak* 8 | lib 9 | coverage 10 | output 11 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "camelcase": true, 4 | "devel": true, 5 | "expr": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "forin": true, 9 | "immed": true, 10 | "latedef": "nofunc", 11 | "newcap": true, 12 | "noarg": true, 13 | "nonew": true, 14 | "quotmark": "single", 15 | "sub": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": false, 19 | "globalstrict": true, 20 | "trailing": true, 21 | "browser": true, 22 | "maxparams": 5, 23 | "maxdepth": 5, 24 | "maxstatements": 25, 25 | "maxcomplexity": 10, 26 | "laxbreak": true, 27 | "node": true, 28 | "predef": [ 29 | "System" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | npm-debug.log 4 | Thumbs.db 5 | .DS_Store 6 | *.swp 7 | *.bak 8 | src 9 | .babelignore 10 | .gitignore 11 | .npmignore 12 | coverage 13 | output 14 | test 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "4" 5 | - "6" 6 | after_script: 7 | - npm run coveralls 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | === 3 | 4 | #### 2016.09.24 5 | 6 | 1. 配置文件统一为 .csshintrc(JSON or YAML) 7 | 8 | #### 2016.09.19 9 | 10 | 1. 这次更新支持了 YAML 配置,同时切换成 ES6。 11 | 12 | #### 2016.07.31 13 | 14 | 1. 在 `block-indent` 规则的检测中,在自定义 block-indent 规则的时候,计算选择器里面属性的开头缩进位置时,之前会把选择器换行符之后的 `\s` 全部去掉,会导致选择器里面的属性的缩进开头位置始终从头开始,例如下面的代码中正确的结果应该是检测成功的,但由于前面的问题会导致误报: 15 | 16 | /* csshint-disable no-bom */ /* csshint block-indent: [" ", 4] */ 17 | 18 | body { 19 | margin: 0; 20 | padding: 0; 21 | } 22 | 23 | 新版本修复了这个问题。 24 | 25 | #### 2016.04.06 26 | 27 | 1. 在 `disallow-overqualified-elements` 规则的检测中,类似 10.5% 这样的选择器(主要会出现在 keyframes 中)会误报 28 | 29 | @keyframes 'test' { 30 | 10.5% { 31 | opacity: 0; 32 | } 33 | } 34 | 35 | 新版本修复了这个问题。 36 | 37 | #### 2016.03.28 38 | 39 | 1. 在 `block-indent` 规则的检测中,如果遇到如下情况**(注意 `div` 的 `}` 符号之后存在空格)**,会出现误报: 40 | 41 | div { 42 | color: #fff; 43 | } 44 | span { 45 | color: #000; 46 | } 47 | 48 | 新版本修复了这个问题。 49 | 50 | #### 2015.11.03 51 | 52 | 1. 修复了一个 `block-indent` 规则的 bug,在 `block-indent` 规则的检测中,为了防止同一行多次报错设置了 `lineCache` 来记录报错的行号,但是在本规则初始化没有还原 `lineCache`,会导致在多次调用的情况下,同一行的 `block-indent` 规则只会报出一次,之后便不再报出来了。这次修复了这个问题。 53 | 2. 去掉了代码中无用的注释以及一些调试代码~。 54 | 55 | #### 2015.09.08 56 | 57 | 1. 修复了一个 `block-indent` 规则的 bug,在 `block-indent` 规则的检测中,如下 css 属性值(即属性值后有多个无用的分号)会误报 `background-position-x: 170px;;` 58 | 59 | #### 2015.07.14 60 | 61 | 1. 覆盖了 CSSLint 的[规则](https://github.com/CSSLint/csslint/wiki/Rules) 62 | 63 | #### 2015.07.03 64 | 65 | 1. 在 2015.06.25 的那次修复中,`vendor-prefixes-sort` 的粒度还是不够,之前没有考虑到 `@keyframes` 的情况,导致如下代码会误报: 66 | 67 | @-webkit-keyframes link_float { 68 | from { 69 | opacity: 0; 70 | -webkit-transform: scale(0); 71 | transform: scale(0); 72 | } 73 | } 74 | @-moz-keyframes link_float { 75 | from { 76 | -moz-transform: scale(0); 77 | transform: scale(0); 78 | opacity: 0; 79 | } 80 | } 81 | 82 | 83 | 新版本修复了这个问题。 84 | 85 | #### 2015.06.25 86 | 87 | 1. 修复了一个 `vendor-prefixes-sort` 规则的 bug,在 `vendor-prefixes-sort` 的判断中,是以类的粒度来判断属性的,但是如果是如下情况,那么之前的版本会误报: 88 | 89 | .modal-content { 90 | -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5); 91 | box-shadow: 0 3px 9px rgba(0, 0, 0, .5); 92 | } 93 | 94 | @media (min-width: 768px) { 95 | .modal-content { 96 | -webkit-box-shadow: 0 5px 15px rgba(100, 0, 0, .5); 97 | box-shadow: 0 5px 15px rgba(100, 0, 0, .5); 98 | } 99 | @media (min-width: 168px) { 100 | .modal-content { 101 | -webkit-box-shadow: 0 8px 19px rgba(0, 200, 0, .5); 102 | box-shadow: 0 8px 19px rgba(0, 200, 0, .5); 103 | } 104 | } 105 | } 106 | 107 | 新版本修复了这个问题。 108 | 109 | #### 2015.06.16 110 | 111 | 1. 修改了 `block-indent` 的实现逻辑,增加了 `block-indent` 对选择器以及嵌套的 @ 选择器(如 `@media`, `@keyframes`)的检测。 112 | 113 | #### 2015.06.15 114 | 115 | 1. `block-indent` 规则的配置修改为 `[" ", 4]` 或者 `["\t", 4]`,其中数组的第零项指的是缩进的字符串,第一项指的的缩进的开始位置,这个开始位置和前面的缩进的字符串的长度是没有关系的,可以理解成目标位置和当前行的第零列的位置的差值。例如:`[" ", 0]` 这样的配置表示缩进为 4 个空格,缩进的起始位置第 0 列即开头位置。而 `["\t", 6]` 这样的配置则表示随进为一个 `\t`,缩进的起始位置为第 6 列。 116 | 2. 行内注释更加丰富,现在支持行内注释动态设置规则的配置。例如: 117 | 118 | /* csshint block-indent: [" ", 4], max-length: 3 */ 119 | 120 | 这一行注释把默认的配置 `block-indent: [" ", 0]`,`max-length: 120` 给覆盖掉(仅对当前文件有效),那么在当前文件中,所执行的规则就是按照这个行内注释里的配置来进行检测。 121 | 122 | #### 2015.05.16 123 | 124 | 新的版本,介绍如上。另外,行内注释增加了忽略全部规则,如果在`/* csshint-disable */`中不配置任何规则,那么就会忽略全部规则,例如 125 | 126 | /* csshint-disable */ 127 | 128 | #### 2015.05.04 129 | 130 | 行内注释的规则匹配,支持任意的`[^a-z-]`作为分隔符,例如 131 | 132 | /* csshint-disable always-semicolon, require-newline require-after-space | block-indent */ 133 | 134 | 这段代码可以让当前文件不检测`always-semicolon`, `require-newline`, `require-after-space`以及`block-indent`这四个规则。这四个规则之间的分隔符分别是`,`, ` `, `|` 135 | 136 | #### 2015.05.03 137 | 138 | 提供简单的行内注释。 139 | 140 | 例如在某文件里设置如下代码 141 | 142 | /* csshint-disable always-semicolon, require-newline,require-after-space */ 143 | 144 | 这段代码可以让当前文件不检测`always-semicolon`, `require-newline`, `require-after-space`这三个规则。 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CSSHint 2 | === 3 | [![csshint](https://travis-ci.org/ecomfe/node-csshint.svg?branch=master)](https://travis-ci.org/ecomfe/node-csshint) 4 | [![npm version](https://badge.fury.io/js/csshint.svg)](http://badge.fury.io/js/csshint) 5 | [![Coverage Status](https://img.shields.io/coveralls/ecomfe/node-csshint.svg?style=flat)](https://coveralls.io/r/ecomfe/node-csshint) 6 | [![Dependency Status](https://david-dm.org/ecomfe/node-csshint.png)](https://david-dm.org/ecomfe/node-csshint) 7 | [![devDependency Status](https://david-dm.org/ecomfe/node-csshint/dev-status.png)](https://david-dm.org/ecomfe/node-csshint#info=devDependencies) 8 | 9 | CSSHint is a code review tool based on NodeJS. The current rules are based on ecomfe [CSS STYLE SPEC](https://github.com/ecomfe/spec/blob/master/css-style-guide.md). It also covers [CSSLint](https://github.com/CSSLint/csslint) [rules](https://github.com/CSSLint/csslint/wiki/Rules). 10 | 11 | After a period of code refactoring, we finally came to this version, in which `CSS` parser is replaced with [postcss](https://github.com/postcss/postcss). 12 | In addition, we changed the way of implementation which was also able to improve the performance by a large margin. Meanwhile, the following `global` object has three attributes as follows: 13 | 14 | - `global.CSSHINT_INVALID_ALL_COUNT`: it is used to count the number of `warn` to serve `max-error`. 15 | - `global.CSSHINT_HEXCOLOR_CASE_FLAG`: it is used to record project's color value, whether the letters are small or capital. `0` is for small and `1` is for capital. This attribute is to serve `unifying-color-case-sensitive`. 16 | - `global.CSSHINT_FONTFAMILY_CASE_FLAG`: it is used to record whether `font-family` is small or capital to serve `unifying-font-family-case-sensitive`. 17 | 18 | [CONFIG Reference](https://github.com/ecomfe/node-csshint/blob/master/lib/config.js) 19 | 20 | 21 | Install & Update 22 | ------- 23 | 24 | CSSHint has been released on npm. It can be installed following the instructions. 25 | 26 | $ [sudo] npm install csshint [-g] 27 | 28 | Follow the following instruction if you are to update your CSSHint. 29 | 30 | $ [sudo] npm update csshint [-g] 31 | 32 | 33 | Usage 34 | ------ 35 | 36 | - in CLI 37 | 38 | $ csshint -v // show version 39 | $ csshint [filePath|dirPath] // run csshint on file or dir 40 | 41 | - in Node.js 42 | 43 | /** 44 | * detect css file content 45 | * 46 | * @param {string} fileContent file content 47 | * @param {Object=} config config of rule, optional 48 | * 49 | * @return {Promise} Promise Object 50 | * reject and resolve arguments: 51 | * { 52 | * path: {string} file path 53 | * messages: {Array.} warning messages, [{ruleName, line, col, errorChar, message, colorMessage}] 54 | * } 55 | */ 56 | exports.checkString(fileContent, config); 57 | 58 | 59 | /** 60 | * detect file 61 | * 62 | * @param {Object} file the object has path and content key 63 | * @param {Array} errors warning messages 64 | * @param {Function} done detect callback 65 | */ 66 | check(file, errors, done); 67 | 68 | 69 | TODO 70 | ------ 71 | 72 | - [x] Complete coverage [csslint](https://github.com/CSSLint/csslint) [rule](https://github.com/CSSLint/csslint/wiki/Rules)。 73 | - [ ] support `/* csshint-disable ruleName */` and `/* csshint-enable ruleName1 */`. 74 | 75 | ### [CHANGELOG](https://github.com/ecomfe/node-csshint/blob/master/CHANGELOG.md) 76 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | CSSHint 2 | === 3 | [![csshint](https://travis-ci.org/ecomfe/node-csshint.svg?branch=master)](https://travis-ci.org/ecomfe/node-csshint) 4 | [![npm version](https://badge.fury.io/js/csshint.svg)](http://badge.fury.io/js/csshint) 5 | [![Coverage Status](https://img.shields.io/coveralls/ecomfe/node-csshint.svg?style=flat)](https://coveralls.io/r/ecomfe/node-csshint) 6 | [![Dependency Status](https://david-dm.org/ecomfe/node-csshint.png)](https://david-dm.org/ecomfe/node-csshint) 7 | [![devDependency Status](https://david-dm.org/ecomfe/node-csshint/dev-status.png)](https://david-dm.org/ecomfe/node-csshint#info=devDependencies) 8 | 9 | CSSHint 是一个基于 NodeJS 的代码规范审查工具,目前的规则是基于 ecomfe 的 [CSS 编码规范](https://github.com/ecomfe/spec/blob/master/css-style-guide.md),同时也覆盖了 [CSSLint](https://github.com/CSSLint/csslint) 的[规则](https://github.com/CSSLint/csslint/wiki/Rules)。 10 | 11 | 经过了一段时间的重构,终于来到这个版本。在这个版本中,`css`解析器切换成 [postcss](https://github.com/postcss/postcss)。此外,这个版本里,改变了实现方式,性能较以前的版本有比较大的提升。同时,在全局`global`对象上挂载了如下三个属性: 12 | 13 | - `global.CSSHINT_INVALID_ALL_COUNT`: 用于记录全局的`warn`个数,为`max-error`规则服务。 14 | - `global.CSSHINT_HEXCOLOR_CASE_FLAG`: 记录项目级别的颜色值的大小写信息,0: 小写, 1: 大写,为`unifying-color-case-sensitive`规则服务。 15 | - `global.CSSHINT_FONTFAMILY_CASE_FLAG`: 记录项目级别的`font-family`大小写信息,为`unifying-font-family-case-sensitive`规则服务。 16 | 17 | [配置参考](https://github.com/ecomfe/node-csshint/blob/master/lib/config.js) 18 | 19 | 20 | Install & Update 21 | ------- 22 | 23 | CSSHint 已发布到 npm 上,可通过如下命令安装。 24 | 25 | $ [sudo] npm install csshint [-g] 26 | 27 | 升级 CSSHint 请用如下命令。 28 | 29 | $ [sudo] npm update csshint [-g] 30 | 31 | 32 | Usage 33 | ------ 34 | 35 | - in CLI 36 | 37 | $ csshint -v // 显示版本信息 38 | $ csshint [filePath|dirPath] // 对 file 或 dir 执行 csshint 39 | - in Node.js 40 | 41 | /** 42 | * 检测 css 文件内容 43 | * 44 | * @param {string} fileContent 文件内容 45 | * @param {Object=} config 检测规则的配置,可选 46 | * 47 | * @return {Promise} Promise 对象, 48 | * Promise 对象的 reject 和 resolve 的回调函数的参数格式如下, 49 | * { 50 | * path: {string} 文件路径 51 | * messages: {Array.} 错误信息集合,[{ruleName, line, col, errorChar, message, colorMessage}] 52 | * } 53 | */ 54 | exports.checkString(fileContent, config); 55 | 56 | 57 | /** 58 | * 校验文件 59 | * 60 | * @param {Object} file 包含 path, content 键的对象 61 | * @param {Array} errors 本分类的错误信息数组 62 | * @param {Function} done 校验完成的通知回调 63 | */ 64 | check(file, errors, done); 65 | 66 | 67 | TODO 68 | ------ 69 | 70 | - [x] 完全覆盖 [csslint](https://github.com/CSSLint/csslint) 里的[规则](https://github.com/CSSLint/csslint/wiki/Rules)。 71 | - [ ] 支持`/* csshint-enable ruleName */` 这样的配置,这就意味着要让 `/* csshint-disable ruleName1 */` 和 `/* csshint-enable ruleName1 */` 之间的内容满足行内注释的规则配置。 72 | 73 | 74 | ### [CHANGELOG](https://github.com/ecomfe/node-csshint/blob/master/CHANGELOG.md) 75 | -------------------------------------------------------------------------------- /bin/csshint-cli: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | var cli = require('../lib/cli'); 4 | cli.parse(process.argv); 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csshint", 3 | "description": "lint your css code", 4 | "version": "0.3.4", 5 | "keywords": [ 6 | "csslint", 7 | "csshint" 8 | ], 9 | "maintainers": [ 10 | { 11 | "name": "ielgnaw", 12 | "email": "wuji0223@gmail.com" 13 | } 14 | ], 15 | "dependencies": { 16 | "chalk": "^1.1.3", 17 | "edp-core": "^1.0.32", 18 | "js-yaml": "^3.6.1", 19 | "manis": "^0.3.0", 20 | "object-assign": "^4.1.0", 21 | "postcss": "^5.2.0", 22 | "strip-json-comments": "^2.0.1" 23 | }, 24 | "engines": { 25 | "node": ">=0.10" 26 | }, 27 | "devDependencies": { 28 | "babel-cli": "^6.14.0", 29 | "babel-core": "^6.14.0", 30 | "babel-istanbul": "^0.11.0", 31 | "babel-node-debug": "^2.0.0", 32 | "babel-preset-es2015": "^6.14.0", 33 | "babel-preset-stage-2": "^6.13.0", 34 | "chai": "^3.5.0", 35 | "coveralls": "^2.11.13", 36 | "debug": "^2.2.0", 37 | "fecs": "stable", 38 | "istanbul": "^0.3.2", 39 | "jasmine-node": "^1.14.5", 40 | "json-stringify-safe": "^5.0.1", 41 | "mocha": "^3.0.2" 42 | }, 43 | "scripts": { 44 | "lint": "fecs src test/**/*.spec.js --type=js", 45 | "compile": "rm -rf lib && ./node_modules/.bin/babel src -d lib --source-maps inline --copy-files", 46 | "debug": "npm run compile && ./node_modules/.bin/babel-node-debug lib/index.js", 47 | "test": "npm run compile && ./node_modules/.bin/_mocha --compilers js:babel-core/register --recursive", 48 | "coverage": "npm run compile && ./node_modules/.bin/babel-node ./node_modules/.bin/babel-istanbul cover _mocha 'test/**/*.spec.@(js|es|es6)'", 49 | "coverage1": "npm run compile && ./node_modules/.bin/babel-node ./node_modules/.bin/babel-istanbul cover _mocha -- --recursive", 50 | "coveralls": "cat ./coverage/lcov.info | coveralls", 51 | "sourcemap": "./node_modules/.bin/babel src -d lib -s", 52 | "watch": "./node_modules/.bin/babel -w src -d lib --source-maps inline --copy-files", 53 | "prepublish": "npm run compile" 54 | }, 55 | "main": "./lib/checker.js", 56 | "bin": { 57 | "csshint": "./bin/csshint-cli" 58 | }, 59 | "repository": { 60 | "type": "git", 61 | "url": "git@github.com:ecomfe/node-csshint" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 命令行功能模块 3 | * @author ielgnaw(wuji0223@gmail.com) 4 | */ 5 | 6 | import {createReadStream} from 'fs'; 7 | import chalk from 'chalk'; 8 | import {log} from 'edp-core'; 9 | import sys from '../package'; 10 | import {formatMsg, getCandidates} from './util'; 11 | import {check} from './checker'; 12 | 13 | /** 14 | * 显示默认的信息 15 | */ 16 | const showDefaultInfo = () => { 17 | console.log(''); 18 | console.log((sys.name + ' v' + sys.version)); 19 | console.log(chalk.bold.green(formatMsg(sys.description))); 20 | }; 21 | 22 | /** 23 | * 校验结果报告 24 | * 25 | * @inner 26 | * @param {Object} errors 按文件类型为 key,值为对应的校验错误信息列表的对象 27 | */ 28 | const report = errors => { 29 | let t12 = true; 30 | 31 | if (errors.length) { 32 | errors.forEach( 33 | error => { 34 | log.info(error.path); 35 | error.messages.forEach( 36 | message => { 37 | const ruleName = message.ruleName || ''; 38 | let msg = '→ ' + (ruleName ? chalk.bold(ruleName) + ': ' : ''); 39 | // 全局性的错误可能没有位置信息 40 | if (typeof message.line === 'number') { 41 | msg += ('line ' + message.line); 42 | if (typeof message.col === 'number') { 43 | msg += (', col ' + message.col); 44 | } 45 | msg += ': '; 46 | } 47 | 48 | msg += message.colorMessage || message.message; 49 | log.warn(msg); 50 | } 51 | ); 52 | } 53 | ); 54 | t12 = false; 55 | } 56 | 57 | if (t12) { 58 | log.info('Congratulations! Everything gone well, you are T12!'); 59 | } 60 | else { 61 | process.exit(1); 62 | } 63 | }; 64 | 65 | /** 66 | * 解析参数。作为命令行执行的入口 67 | * 68 | * @param {Array} args 参数列表 69 | */ 70 | export function parse(args) { 71 | args = args.slice(2); 72 | 73 | // 不带参数时,默认检测当前目录下所有的 css 文件 74 | if (args.length === 0) { 75 | args.push('.'); 76 | } 77 | 78 | if (args[0] === '--version' || args[0] === '-v') { 79 | showDefaultInfo(); 80 | return; 81 | } 82 | 83 | const patterns = [ 84 | '**/*.css', 85 | '!**/{output,test,node_modules,asset,dist,release,doc,dep,report}/**' 86 | ]; 87 | 88 | const candidates = getCandidates(args, patterns); 89 | let count = candidates.length; 90 | 91 | if (!count) { 92 | return; 93 | } 94 | 95 | // 错误信息的集合 96 | const errors = []; 97 | 98 | /** 99 | * 每个文件的校验结果回调,主要用于统计校验完成情况 100 | * 101 | * @inner 102 | */ 103 | const callback = () => { 104 | count--; 105 | if (!count) { 106 | report(errors); 107 | } 108 | }; 109 | 110 | // 遍历每个需要检测的 css 文件 111 | candidates.forEach(candidate => { 112 | const readable = createReadStream(candidate, { 113 | encoding: 'utf8' 114 | }); 115 | readable.on('data', chunk => { 116 | const file = { 117 | content: chunk, 118 | path: candidate 119 | }; 120 | check(file, errors, callback); 121 | }); 122 | readable.on('error', err => { 123 | throw err; 124 | }); 125 | }); 126 | } 127 | -------------------------------------------------------------------------------- /src/colors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file css 颜色值 http://www.w3school.com.cn/cssref/css_colornames.asp 3 | * @author ielgnaw(wuji0223@gmail.com) 4 | */ 5 | 6 | export default { 7 | aliceblue: '#f0f8ff', 8 | antiquewhite: '#faebd7', 9 | aqua: '#00ffff', 10 | aquamarine: '#7fffd4', 11 | azure: '#f0ffff', 12 | beige: '#f5f5dc', 13 | bisque: '#ffe4c4', 14 | black: '#000000', 15 | blanchedalmond: '#ffebcd', 16 | blue: '#0000ff', 17 | blueviolet: '#8a2be2', 18 | brown: '#a52a2a', 19 | burlywood: '#deb887', 20 | cadetblue: '#5f9ea0', 21 | chartreuse: '#7fff00', 22 | chocolate: '#d2691e', 23 | coral: '#ff7f50', 24 | cornflowerblue: '#6495ed', 25 | cornsilk: '#fff8dc', 26 | crimson: '#dc143c', 27 | cyan: '#00ffff', 28 | darkblue: '#00008b', 29 | darkcyan: '#008b8b', 30 | darkgoldenrod: '#b8860b', 31 | darkgray: '#a9a9a9', 32 | darkgreen: '#006400', 33 | darkkhaki: '#bdb76b', 34 | darkmagenta: '#8b008b', 35 | darkolivegreen: '#556b2f', 36 | darkorange: '#ff8c00', 37 | darkorchid: '#9932cc', 38 | darkred: '#8b0000', 39 | darksalmon: '#e9967a', 40 | darkseagreen: '#8fbc8f', 41 | darkslateblue: '#483d8b', 42 | darkslategray: '#2f4f4f', 43 | darkturquoise: '#00ced1', 44 | darkviolet: '#9400d3', 45 | deeppink: '#ff1493', 46 | deepskyblue: '#00bfff', 47 | dimgray: '#696969', 48 | dodgerblue: '#1e90ff', 49 | feldspar: '#d19275', 50 | firebrick: '#b22222', 51 | floralwhite: '#fffaf0', 52 | forestgreen: '#228b22', 53 | fuchsia: '#ff00ff', 54 | gainsboro: '#dcdcdc', 55 | ghostwhite: '#f8f8ff', 56 | gold: '#ffd700', 57 | goldenrod: '#daa520', 58 | gray: '#808080', 59 | green: '#008000', 60 | greenyellow: '#adff2f', 61 | honeydew: '#f0fff0', 62 | hotpink: '#ff69b4', 63 | indianred: '#cd5c5c', 64 | indigo: '#4b0082', 65 | ivory: '#fffff0', 66 | khaki: '#f0e68c', 67 | lavender: '#e6e6fa', 68 | lavenderblush: '#fff0f5', 69 | lawngreen: '#7cfc00', 70 | lemonchiffon: '#fffacd', 71 | lightblue: '#add8e6', 72 | lightcoral: '#f08080', 73 | lightcyan: '#e0ffff', 74 | lightgoldenrodyellow: '#fafad2', 75 | lightgrey: '#d3d3d3', 76 | lightgreen: '#90ee90', 77 | lightpink: '#ffb6c1', 78 | lightsalmon: '#ffa07a', 79 | lightseagreen: '#20b2aa', 80 | lightskyblue: '#87cefa', 81 | lightslateblue: '#8470ff', 82 | lightslategray: '#789', 83 | lightsteelblue: '#b0c4de', 84 | lightyellow: '#ffffe0', 85 | lime: '#00ff00', 86 | limegreen: '#32cd32', 87 | linen: '#faf0e6', 88 | magenta: '#ff00ff', 89 | maroon: '#800000', 90 | mediumaquamarine: '#66cdaa', 91 | mediumblue: '#0000cd', 92 | mediumorchid: '#ba55d3', 93 | mediumpurple: '#9370d8', 94 | mediumseagreen: '#3cb371', 95 | mediumslateblue: '#7b68ee', 96 | mediumspringgreen: '#00fa9a', 97 | mediumturquoise: '#48d1cc', 98 | mediumvioletred: '#c71585', 99 | midnightblue: '#191970', 100 | mintcream: '#f5fffa', 101 | mistyrose: '#ffe4e1', 102 | moccasin: '#ffe4b5', 103 | navajowhite: '#ffdead', 104 | navy: '#000080', 105 | oldlace: '#fdf5e6', 106 | olive: '#808000', 107 | olivedrab: '#6b8e23', 108 | orange: '#ffa500', 109 | orangered: '#ff4500', 110 | orchid: '#da70d6', 111 | palegoldenrod: '#eee8aa', 112 | palegreen: '#98fb98', 113 | paleturquoise: '#afeeee', 114 | palevioletred: '#d87093', 115 | papayawhip: '#ffefd5', 116 | peachpuff: '#ffdab9', 117 | peru: '#cd853f', 118 | pink: '#ffc0cb', 119 | plum: '#dda0dd', 120 | powderblue: '#b0e0e6', 121 | purple: '#800080', 122 | red: '#ff0000', 123 | rosybrown: '#bc8f8f', 124 | royalblue: '#4169e1', 125 | saddlebrown: '#8b4513', 126 | salmon: '#fa8072', 127 | sandybrown: '#f4a460', 128 | seagreen: '#2e8b57', 129 | seashell: '#fff5ee', 130 | sienna: '#a0522d', 131 | silver: '#c0c0c0', 132 | skyblue: '#87ceeb', 133 | slateblue: '#6a5acd', 134 | slategray: '#708090', 135 | snow: '#fffafa', 136 | springgreen: '#00ff7f', 137 | steelblue: '#4682b4', 138 | tan: '#d2b48c', 139 | teal: '#008080', 140 | thistle: '#d8bfd8', 141 | tomato: '#ff6347', 142 | turquoise: '#40e0d0', 143 | violet: '#ee82ee', 144 | violetred: '#d02090', 145 | wheat: '#f5deb3', 146 | white: '#ffffff', 147 | whitesmoke: '#f5f5f5', 148 | yellow: '#ffff00', 149 | yellowgreen: '#9acd32' 150 | }; 151 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 对配置文件的读取合并等等 3 | * @author ielgnaw(wuji0223@gmail.com) 4 | */ 5 | 6 | import Manis from 'manis'; 7 | import {join} from 'path'; 8 | import yaml from 'js-yaml'; 9 | 10 | 'use strict'; 11 | 12 | let STORAGE = null; 13 | 14 | // const JSON_YAML_REG = /.+\.(json|yml)$/i; 15 | 16 | /** 17 | * 获取 merge 后的配置文件 18 | * 19 | * @param {string} filePath 待检查的文件路径,根据这个路径去寻找用户自定义的配置文件,然后和默认的配置文件 merge 20 | * @param {boolean} refresh 是否强制刷新内存中已经存在的配置 21 | * 22 | * @return {Object} merge 后的配置对象 23 | */ 24 | export function loadConfig(filePath, refresh) { 25 | if (!refresh && STORAGE) { 26 | return STORAGE; 27 | } 28 | 29 | let manis = new Manis({ 30 | files: [ 31 | '.csshintrc' 32 | ], 33 | loader(content) { 34 | if (!content) { 35 | return ''; 36 | } 37 | let ret; 38 | try { 39 | ret = yaml.load(content); 40 | } 41 | catch (e) {} 42 | // if (basename(filePath) === '.csshintrc') { 43 | // return JSON.parse(stripJSONComments(content)); 44 | // } 45 | // 46 | // let match = filePath.match(JSON_YAML_REG); 47 | // 48 | // if (match) { 49 | // let suffix = match[1]; 50 | // if (suffix === 'json') { 51 | // return JSON.parse(stripJSONComments(content)); 52 | // } 53 | // else if (suffix === 'yml') { 54 | // return yaml.load(content); 55 | // } 56 | // } 57 | return ret; 58 | } 59 | }); 60 | 61 | manis.setDefault(join(__dirname, './csshint.yml')); 62 | 63 | STORAGE = manis.from(filePath); 64 | 65 | return STORAGE; 66 | } 67 | -------------------------------------------------------------------------------- /src/prefixes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file prefixes 3 | * @author ielgnaw(wuji0223@gmail.com) 4 | */ 5 | 6 | /** 7 | * 所有的 prefixes 8 | * http://peter.sh/experiments/vendor-prefixed-css-property-overview/ 9 | * value 后有空格说明有标准模式 10 | * 11 | * @type {Object} 12 | */ 13 | const prefixes = { 14 | /* eslint-disable fecs-camelcase */ 15 | 16 | 'animation': 'webkit moz ', 17 | 'animation-delay': 'webkit moz ', 18 | 'animation-direction': 'webkit moz ', 19 | 'animation-duration': 'webkit moz ', 20 | 'animation-fill-mode': 'webkit moz ', 21 | 'animation-iteration-count': 'webkit moz ', 22 | 'animation-name': 'webkit moz ', 23 | 'animation-play-state': 'webkit moz ', 24 | 'animation-timing-function': 'webkit moz ', 25 | 'appearance': 'webkit moz ', 26 | 'border-end': 'webkit moz ', 27 | 'border-end-color': 'webkit moz ', 28 | 'border-end-style': 'webkit moz ', 29 | 'border-end-width': 'webkit moz ', 30 | 'border-image': 'webkit moz o ', 31 | 'border-radius': 'webkit moz ', 32 | 'border-start': 'webkit moz ', 33 | 'border-start-color': 'webkit moz ', 34 | 'border-start-style': 'webkit moz ', 35 | 'border-start-width': 'webkit moz ', 36 | 'box-align': 'webkit moz ms ', 37 | 'box-direction': 'webkit moz ms ', 38 | 'box-flex': 'webkit moz ms ', 39 | 'box-lines': 'webkit ms ', 40 | 'box-ordinal-group': 'webkit moz ms ', 41 | 'box-orient': 'webkit moz ms ', 42 | 'box-pack': 'webkit moz ms ', 43 | 'box-sizing': 'webkit moz ', 44 | 'box-shadow': 'webkit moz ', 45 | 'column-count': 'webkit moz ms ', 46 | 'column-gap': 'webkit moz ms ', 47 | 'column-rule': 'webkit moz ms ', 48 | 'column-rule-color': 'webkit moz ms ', 49 | 'column-rule-style': 'webkit moz ms ', 50 | 'column-rule-width': 'webkit moz ms ', 51 | 'column-width': 'webkit moz ms ', 52 | 'hyphens': 'epub moz ', 53 | 'line-break': 'webkit ms ', 54 | 'margin-end': 'webkit moz ', 55 | 'margin-start': 'webkit moz ', 56 | 'marquee-speed': 'webkit wap ', 57 | 'marquee-style': 'webkit wap ', 58 | 'padding-end': 'webkit moz ', 59 | 'padding-start': 'webkit moz ', 60 | 'tab-size': 'moz o ', 61 | 'text-size-adjust': 'webkit ms ', 62 | 'transform': 'webkit moz ms o ', 63 | 'transform-origin': 'webkit moz ms o ', 64 | 'transition': 'webkit moz o ', 65 | 'transition-delay': 'webkit moz o ', 66 | 'transition-duration': 'webkit moz o ', 67 | 'transition-property': 'webkit moz o ', 68 | 'transition-timing-function': 'webkit moz o ', 69 | 'user-modify': 'webkit moz ', 70 | 'user-select': 'webkit moz ms ', 71 | 'word-break': 'epub ms ', 72 | 'writing-mode': 'epub ms', 73 | 'background-size': 'webkit moz o ', 74 | 'flex': 'webkit moz ms ', 75 | 'filter': 'webkit ', 76 | 'background-clip': 'webkit moz khtml ', 77 | 'justify-content': 'webkit moz ', 78 | 'align-items': 'webkit ', 79 | 'backface-visibility': 'webkit moz ', 80 | 'flex-direction': 'webkit ms ', 81 | 'align-self': 'webkit ' 82 | 83 | /* eslint-enable fecs-camelcase */ 84 | }; 85 | 86 | 87 | const arrayPush = Array.prototype.push; 88 | const allPrefixes = []; 89 | 90 | /* eslint-disable fecs-use-for-of, fecs-valid-map-set */ 91 | for (const prop in prefixes) { 92 | if (prefixes.hasOwnProperty(prop)) { 93 | const variations = []; 94 | const prefixed = prefixes[prop].split(' '); 95 | for (let i = 0, len = prefixed.length; i < len; i++) { 96 | // 标准模式 97 | if (prefixed[i] === '') { 98 | variations.push(prop); 99 | } 100 | else { 101 | variations.push('-' + prefixed[i] + '-' + prop); 102 | } 103 | } 104 | prefixes[prop] = variations; 105 | arrayPush.apply(allPrefixes, variations); 106 | } 107 | } 108 | /* eslint-enable fecs-use-for-of, fecs-valid-map-set */ 109 | 110 | /** 111 | * 获取所有的 prefixes 112 | * 113 | * @return {Array} 集合 114 | */ 115 | export function getPrefixList() { 116 | return allPrefixes; 117 | } 118 | 119 | /** 120 | * 获取所有的 prefixes 121 | * 122 | * @return {Array} 集合 123 | */ 124 | export function getPrefixMap() { 125 | return prefixes; 126 | } 127 | -------------------------------------------------------------------------------- /src/rule/adjoining-classes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file adjoining-classes 的检测逻辑 3 | * Don't use adjoining classes 例如 .foo.bar 4 | * https://github.com/CSSLint/csslint/wiki/Disallow-adjoining-classes 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | import {getLineContent} from '../util'; 12 | 13 | 'use strict'; 14 | 15 | /** 16 | * 当前文件所代表的规则名称 17 | * 18 | * @const 19 | * @type {string} 20 | */ 21 | const RULENAME = 'adjoining-classes'; 22 | 23 | /** 24 | * css 组合的正则匹配 25 | * 26 | * @const 27 | * @type {RegExp} 28 | */ 29 | const PATTERN_COMBINATORS = /[\s>+~,[]+/; 30 | 31 | /** 32 | * 错误信息 33 | * 34 | * @const 35 | * @type {string} 36 | */ 37 | const MSG = 'Don\'t use adjoining classes'; 38 | 39 | /** 40 | * 具体的检测逻辑 41 | * 42 | * @param {Object} opts 参数 43 | * @param {*} opts.ruleVal 当前规则具体配置的值 44 | * @param {string} opts.fileContent 文件内容 45 | * @param {string} opts.filePath 文件路径 46 | */ 47 | export const check = postcss.plugin(RULENAME, opts => 48 | (css, result) => { 49 | if (!opts.ruleVal) { 50 | return; 51 | } 52 | 53 | css.walkRules(rule => { 54 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 55 | return; 56 | } 57 | 58 | const segments = rule.selector.split(PATTERN_COMBINATORS); 59 | 60 | for (let i = 0, len = segments.length; i < len; i++) { 61 | const segment = segments[i]; 62 | if (segment.split('.').length > 2) { 63 | const source = rule.source; 64 | const line = source.start.line; 65 | const lineContent = getLineContent(line, source.input.css) || ''; 66 | const colorStr = segment; 67 | result.warn(RULENAME, { 68 | node: rule, 69 | ruleName: RULENAME, 70 | line: line, 71 | message: MSG, 72 | colorMessage: '`' 73 | + lineContent.replace( 74 | colorStr, 75 | chalk.magenta(colorStr) 76 | ) 77 | + '` ' 78 | + chalk.grey(MSG) 79 | }); 80 | global.CSSHINT_INVALID_ALL_COUNT++; 81 | } 82 | } 83 | }); 84 | } 85 | ); 86 | -------------------------------------------------------------------------------- /src/rule/always-semicolon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file always-semicolon 的检测逻辑 3 | * 012: [强制] 属性定义后必须以分号结尾。 4 | * @author ielgnaw(wuji0223@gmail.com) 5 | */ 6 | 7 | import chalk from 'chalk'; 8 | import postcss from 'postcss'; 9 | 10 | import {getLineContent} from '../util'; 11 | 12 | /** 13 | * 当前文件所代表的规则名称 14 | * 15 | * @const 16 | * @type {string} 17 | */ 18 | const RULENAME = 'always-semicolon'; 19 | 20 | /** 21 | * 错误信息 22 | * 23 | * @const 24 | * @type {string} 25 | */ 26 | const MSG = 'Attribute definition must end with a semicolon'; 27 | 28 | /** 29 | * 具体的检测逻辑 30 | * 31 | * @param {Object} opts 参数 32 | * @param {*} opts.ruleVal 当前规则具体配置的值 33 | * @param {string} opts.fileContent 文件内容 34 | * @param {string} opts.filePath 文件路径 35 | */ 36 | export const check = postcss.plugin(RULENAME, opts => 37 | (css, result) => { 38 | if (!opts.ruleVal) { 39 | return; 40 | } 41 | 42 | css.walkRules(rule => { 43 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 44 | return; 45 | } 46 | 47 | if (rule.raws.semicolon) { 48 | return; 49 | } 50 | 51 | const lastProp = rule.nodes[rule.nodes.length - 1]; 52 | if (lastProp && lastProp.type !== 'comment') { 53 | const source = lastProp.source; 54 | const line = source.start.line; 55 | const lineContent = getLineContent(line, source.input.css) || ''; 56 | 57 | const value = lastProp.important 58 | ? lastProp.value + (lastProp.important ? lastProp.important : ' !important') 59 | : lastProp.value; 60 | 61 | const colorStr = lastProp.prop + lastProp.raws.between + value; 62 | const col = source.start.column + colorStr.length; 63 | 64 | result.warn(RULENAME, { 65 | node: rule, 66 | ruleName: RULENAME, 67 | line: line, 68 | col: col, 69 | message: MSG, 70 | colorMessage: '`' 71 | + lineContent.replace( 72 | colorStr, 73 | chalk.magenta(colorStr) 74 | ) 75 | + '` ' 76 | + chalk.grey(MSG) 77 | }); 78 | global.CSSHINT_INVALID_ALL_COUNT++; 79 | } 80 | }); 81 | } 82 | ); 83 | -------------------------------------------------------------------------------- /src/rule/box-sizing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file box-sizing 的检测逻辑 3 | * The box-sizing properties isn't supported in IE6 and IE7 4 | * https://github.com/CSSLint/csslint/wiki/Disallow-box-sizing 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | import {getLineContent} from '../util'; 12 | 13 | /** 14 | * 当前文件所代表的规则名称 15 | * 16 | * @const 17 | * @type {string} 18 | */ 19 | const RULENAME = 'box-sizing'; 20 | 21 | /** 22 | * 错误的信息 23 | * 24 | * @const 25 | * @type {string} 26 | */ 27 | const MSG = 'The box-sizing properties isn\'t supported in IE6 and IE7'; 28 | 29 | /** 30 | * 具体的检测逻辑 31 | * 32 | * @param {Object} opts 参数 33 | * @param {*} opts.ruleVal 当前规则具体配置的值 34 | * @param {string} opts.fileContent 文件内容 35 | * @param {string} opts.filePath 文件路径 36 | */ 37 | export const check = postcss.plugin(RULENAME, opts => 38 | (css, result) => { 39 | if (!opts.ruleVal) { 40 | return; 41 | } 42 | 43 | css.walkRules(rule => { 44 | rule.walkDecls(decl => { 45 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 46 | return; 47 | } 48 | 49 | const prop = decl.prop; 50 | if (prop === 'box-sizing') { 51 | const source = decl.source; 52 | const line = source.start.line; 53 | const lineContent = getLineContent(line, source.input.css); 54 | const col = source.start.column; 55 | result.warn(RULENAME, { 56 | node: rule, 57 | ruleName: RULENAME, 58 | line: line, 59 | col: col, 60 | message: MSG, 61 | colorMessage: '`' 62 | + lineContent.replace( 63 | prop, 64 | chalk.magenta(prop) 65 | ) 66 | + '` ' 67 | + chalk.grey(MSG) 68 | }); 69 | global.CSSHINT_INVALID_ALL_COUNT++; 70 | } 71 | }); 72 | }); 73 | } 74 | ); 75 | -------------------------------------------------------------------------------- /src/rule/bulletproof-font-face.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file bulletproof-font-face 的检测逻辑 3 | * Rule: Use the bulletproof @font-face syntax to avoid 404's in old IE 4 | * (http://www.fontspring.com/blog/the-new-bulletproof-font-face-syntax) 5 | * https://github.com/CSSLint/csslint/wiki/Bulletproof-font-face 6 | * @author ielgnaw(wuji0223@gmail.com) 7 | */ 8 | 9 | import chalk from 'chalk'; 10 | import postcss from 'postcss'; 11 | 12 | import {getLineContent, changeColorByStartAndEndIndex} from '../util'; 13 | 14 | /** 15 | * 当前文件所代表的规则名称 16 | * 17 | * @const 18 | * @type {string} 19 | */ 20 | const RULENAME = 'bulletproof-font-face'; 21 | 22 | const PATTERN = /^\s?url\(['"].+\.eot\?.*['"]\)\s*format\(['"]embedded-opentype['"]\)[\s\S]*$/i; 23 | 24 | /** 25 | * 错误的信息 26 | * 27 | * @const 28 | * @type {string} 29 | */ 30 | const MSG = '@font-face declaration doesn\'t follow the fontspring bulletproof syntax'; 31 | 32 | let firstSrc = true; 33 | let failedDecl = false; 34 | 35 | /** 36 | * 具体的检测逻辑 37 | * 38 | * @param {Object} opts 参数 39 | * @param {*} opts.ruleVal 当前规则具体配置的值 40 | * @param {string} opts.fileContent 文件内容 41 | * @param {string} opts.filePath 文件路径 42 | */ 43 | export const check = postcss.plugin(RULENAME, opts => 44 | (css, result) => { 45 | 46 | if (opts.ruleVal) { 47 | css.walkAtRules(atRule => { 48 | if (atRule.name !== 'font-face') { 49 | return; 50 | } 51 | 52 | firstSrc = true; 53 | failedDecl = false; 54 | 55 | atRule.walkDecls(decl => { 56 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 57 | return; 58 | } 59 | 60 | const {prop, value} = decl; 61 | 62 | if (prop === 'src') { 63 | if (!value.match(PATTERN) && firstSrc) { 64 | failedDecl = decl; 65 | firstSrc = false; 66 | } 67 | else if (value.match(PATTERN) && !firstSrc) { 68 | failedDecl = false; 69 | } 70 | } 71 | }); 72 | 73 | if (failedDecl) { 74 | const source = failedDecl.source; 75 | const line = source.start.line; 76 | const lineContent = getLineContent(line, source.input.css); 77 | const col = source.start.column; 78 | result.warn(RULENAME, { 79 | node: atRule, 80 | ruleName: RULENAME, 81 | line: line, 82 | col: col, 83 | message: '`' 84 | + lineContent 85 | + '` ' 86 | + MSG, 87 | colorMessage: '`' 88 | + changeColorByStartAndEndIndex( 89 | lineContent, col, source.end.column 90 | ) 91 | + '` ' 92 | + chalk.grey(MSG) 93 | }); 94 | 95 | global.CSSHINT_INVALID_ALL_COUNT++; 96 | } 97 | }); 98 | } 99 | } 100 | ); 101 | -------------------------------------------------------------------------------- /src/rule/disallow-expression.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file disallow-expression 的检测逻辑 3 | * 050: [强制] 禁止使用 `Expression`。 4 | * @author ielgnaw(wuji0223@gmail.com) 5 | */ 6 | 7 | import chalk from 'chalk'; 8 | import postcss from 'postcss'; 9 | 10 | import {getLineContent} from '../util'; 11 | 12 | /** 13 | * 匹配 css 表达式的正则 14 | * 15 | * @const 16 | * @type {RegExp} 17 | */ 18 | const PATTERN_EXP = /expression\(/i; 19 | 20 | /** 21 | * 当前文件所代表的规则名称 22 | * 23 | * @const 24 | * @type {string} 25 | */ 26 | const RULENAME = 'disallow-expression'; 27 | 28 | /** 29 | * 错误的信息 30 | * 31 | * @const 32 | * @type {string} 33 | */ 34 | const MSG = 'Disallow use `Expression`'; 35 | 36 | /** 37 | * 具体的检测逻辑 38 | * 39 | * @param {Object} opts 参数 40 | * @param {*} opts.ruleVal 当前规则具体配置的值 41 | * @param {string} opts.fileContent 文件内容 42 | * @param {string} opts.filePath 文件路径 43 | */ 44 | export const check = postcss.plugin(RULENAME, opts => 45 | (css, result) => { 46 | if (opts.ruleVal) { 47 | css.walkDecls(decl => { 48 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 49 | return; 50 | } 51 | 52 | const parts = postcss.list.space(decl.value); 53 | for (let i = 0, len = parts.length; i < len; i++) { 54 | const part = parts[i]; 55 | if (PATTERN_EXP.test(part)) { 56 | const source = decl.source; 57 | const line = source.start.line; 58 | const lineContent = getLineContent(line, source.input.css); 59 | const col = source.start.column + decl.prop.length + decl.raws.between.length; 60 | result.warn(RULENAME, { 61 | node: decl, 62 | ruleName: RULENAME, 63 | line: line, 64 | col: col, 65 | message: MSG, 66 | colorMessage: '`' 67 | + lineContent.replace(/expression/g, chalk.magenta('expression')) 68 | + '` ' 69 | + chalk.grey(MSG) 70 | }); 71 | global.CSSHINT_INVALID_ALL_COUNT++; 72 | continue; 73 | } 74 | } 75 | 76 | }); 77 | } 78 | } 79 | ); 80 | -------------------------------------------------------------------------------- /src/rule/disallow-important.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file disallow-important 的检测逻辑 3 | * 019: [建议] 尽量不使用 `!important` 声明。 4 | * @author ielgnaw(wuji0223@gmail.com) 5 | */ 6 | 7 | import chalk from 'chalk'; 8 | import postcss from 'postcss'; 9 | 10 | import {getLineContent} from '../util'; 11 | 12 | /** 13 | * 当前文件所代表的规则名称 14 | * 15 | * @const 16 | * @type {string} 17 | */ 18 | const RULENAME = 'disallow-important'; 19 | 20 | /** 21 | * 错误的信息 22 | * 23 | * @const 24 | * @type {string} 25 | */ 26 | const MSG = 'Try not to use the `important` statement'; 27 | 28 | /** 29 | * 记录行号的临时变量,例如 30 | * color:red !important;height: 100px !important; 31 | * 这段 css ,希望的是这一行只报一次 !important 的错误,这一次把这一行里面的 !important 全部高亮 32 | * 33 | * @type {number} 34 | */ 35 | let lineCache = 0; 36 | 37 | /** 38 | * 具体的检测逻辑 39 | * 40 | * @param {Object} opts 参数 41 | * @param {*} opts.ruleVal 当前规则具体配置的值 42 | * @param {string} opts.fileContent 文件内容 43 | * @param {string} opts.filePath 文件路径 44 | */ 45 | export const check = postcss.plugin(RULENAME, opts => 46 | (css, result) => { 47 | if (opts.ruleVal) { 48 | 49 | lineCache = 0; 50 | 51 | css.walkDecls(decl => { 52 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 53 | return; 54 | } 55 | if (decl.important) { 56 | const source = decl.source; 57 | const line = source.start.line; 58 | 59 | // lineCache === line 时,说明是同一行的,那么就不报了 60 | if (lineCache !== line) { 61 | lineCache = line; 62 | const lineContent = getLineContent(line, source.input.css) || ''; 63 | result.warn(RULENAME, { 64 | node: decl, 65 | ruleName: RULENAME, 66 | line: line, 67 | message: MSG, 68 | colorMessage: '`' 69 | + lineContent.replace( 70 | /!important/gi, 71 | chalk.magenta('!important') 72 | ) 73 | + '` ' 74 | + chalk.grey(MSG) 75 | }); 76 | global.CSSHINT_INVALID_ALL_COUNT++; 77 | } 78 | } 79 | }); 80 | } 81 | } 82 | ); 83 | -------------------------------------------------------------------------------- /src/rule/disallow-named-color.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file disallow-named-color 的检测逻辑 3 | * 031: [强制] 颜色值不允许使用命名色值。 4 | * @author ielgnaw(wuji0223@gmail.com) 5 | */ 6 | 7 | import chalk from 'chalk'; 8 | import postcss from 'postcss'; 9 | 10 | import {getLineContent, changeColorByStartAndEndIndex} from '../util'; 11 | import colors from '../colors'; 12 | 13 | /** 14 | * 当前文件所代表的规则名称 15 | * 16 | * @const 17 | * @type {string} 18 | */ 19 | const RULENAME = 'disallow-named-color'; 20 | 21 | /** 22 | * 错误的信息 23 | * 24 | * @const 25 | * @type {string} 26 | */ 27 | const MSG = 'Color values using named color value is not allowed'; 28 | 29 | /** 30 | * 具体的检测逻辑 31 | * 32 | * @param {Object} opts 参数 33 | * @param {*} opts.ruleVal 当前规则具体配置的值 34 | * @param {string} opts.fileContent 文件内容 35 | * @param {string} opts.filePath 文件路径 36 | */ 37 | export const check = postcss.plugin(RULENAME, opts => 38 | (css, result) => { 39 | 40 | if (opts.ruleVal) { 41 | 42 | css.walkDecls(decl => { 43 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 44 | return; 45 | } 46 | 47 | const parts = postcss.list.space(decl.value); 48 | for (let i = 0, len = parts.length; i < len; i++) { 49 | const part = parts[i]; 50 | if (colors.hasOwnProperty(part)) { 51 | const source = decl.source; 52 | const line = source.start.line; 53 | const lineContent = getLineContent(line, source.input.css); 54 | const extraLine = decl.value.indexOf(part) || 0; 55 | const col = source.start.column + decl.prop.length + decl.raws.between.length + extraLine; 56 | result.warn(RULENAME, { 57 | node: decl, 58 | ruleName: RULENAME, 59 | line: line, 60 | col: col, 61 | message: MSG, 62 | colorMessage: '`' 63 | + changeColorByStartAndEndIndex( 64 | lineContent, col, source.end.column 65 | ) 66 | + '` ' 67 | + chalk.grey(MSG) 68 | }); 69 | global.CSSHINT_INVALID_ALL_COUNT++; 70 | } 71 | } 72 | }); 73 | } 74 | } 75 | ); 76 | -------------------------------------------------------------------------------- /src/rule/disallow-overqualified-elements.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file disallow-overqualified-elements 的检测逻辑 3 | * 013: [强制] 如无必要,不得为 `id`、`class` 选择器添加类型选择器进行限定。 4 | * @author ielgnaw(wuji0223@gmail.com) 5 | */ 6 | 7 | import chalk from 'chalk'; 8 | import postcss from 'postcss'; 9 | 10 | import {getLineContent} from '../util'; 11 | 12 | /** 13 | * 当前文件所代表的规则名称 14 | * 15 | * @const 16 | * @type {string} 17 | */ 18 | const RULENAME = 'disallow-overqualified-elements'; 19 | 20 | /** 21 | * css 组合的正则匹配 22 | * 23 | * @const 24 | * @type {RegExp} 25 | */ 26 | const PATTERN_COMBINATORS = /[\s>+~,[]+/; 27 | 28 | /** 29 | * css selector 开始字符的正则匹配 30 | * 31 | * @const 32 | * @type {RegExp} 33 | */ 34 | const PATTERN_STARTCHARS = /[\.#\[]+/; 35 | 36 | const PATTERN_PERCENT = /^((-|\+)?\d{1,2}(\.\d+)?|100)%$/; 37 | 38 | /** 39 | * 错误信息 40 | * 41 | * @const 42 | * @type {string} 43 | */ 44 | const MSG = 'Not allowed to add a type selector is limited to ID, class selector'; 45 | 46 | /** 47 | * 具体的检测逻辑 48 | * 49 | * @param {Object} opts 参数 50 | * @param {*} opts.ruleVal 当前规则具体配置的值 51 | * @param {string} opts.fileContent 文件内容 52 | * @param {string} opts.filePath 文件路径 53 | */ 54 | export const check = postcss.plugin(RULENAME, opts => 55 | (css, result) => { 56 | if (opts.ruleVal) { 57 | 58 | css.walkRules(rule => { 59 | 60 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 61 | return; 62 | } 63 | 64 | if (!isNaN(rule.selector) || PATTERN_PERCENT.test(rule.selector)) { 65 | return; 66 | } 67 | 68 | const source = rule.source; 69 | const line = source.start.line; 70 | 71 | const lineContent = getLineContent(line, source.input.css) || ''; 72 | 73 | const segments = rule.selector.split(PATTERN_COMBINATORS); 74 | for (let i = 0, len = segments.length; i < len; i++) { 75 | const items = segments[i].split(PATTERN_STARTCHARS); 76 | if (items[0] !== '' && items.length > 1) { 77 | result.warn(RULENAME, { 78 | node: rule, 79 | ruleName: RULENAME, 80 | line: line, 81 | message: MSG, 82 | colorMessage: '`' 83 | + lineContent.replace( 84 | segments[i], 85 | segments[i].replace(items[0], chalk.magenta(items[0])) 86 | ) 87 | + '` ' 88 | + chalk.grey(MSG) 89 | }); 90 | global.CSSHINT_INVALID_ALL_COUNT++; 91 | } 92 | } 93 | }); 94 | } 95 | } 96 | ); 97 | -------------------------------------------------------------------------------- /src/rule/disallow-quotes-in-url.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file disallow-quotes-in-url 的检测逻辑 3 | * 026: [强制] `url()` 函数中的路径不加引号。 4 | * @author ielgnaw(wuji0223@gmail.com) 5 | */ 6 | 7 | import chalk from 'chalk'; 8 | import postcss from 'postcss'; 9 | 10 | import {getLineContent} from '../util'; 11 | 12 | /** 13 | * 当前文件所代表的规则名称 14 | * 15 | * @const 16 | * @type {string} 17 | */ 18 | const RULENAME = 'disallow-quotes-in-url'; 19 | 20 | /** 21 | * 匹配 css 中 url 的正则 22 | * 23 | * @const 24 | * @type {RegExp} 25 | */ 26 | const PATTERN_URL = /\burl\s*\((["']?)([^\)]+)\1\)/g; 27 | 28 | /** 29 | * 错误信息 30 | * 31 | * @const 32 | * @type {string} 33 | */ 34 | const MSG = 'Path in the `url()` must without the quotes'; 35 | 36 | /** 37 | * 具体的检测逻辑 38 | * 39 | * @param {Object} opts 参数 40 | * @param {*} opts.ruleVal 当前规则具体配置的值 41 | * @param {string} opts.fileContent 文件内容 42 | * @param {string} opts.filePath 文件路径 43 | */ 44 | export const check = postcss.plugin(RULENAME, opts => 45 | (css, result) => { 46 | if (opts.ruleVal) { 47 | 48 | css.walkDecls(decl => { 49 | 50 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 51 | return; 52 | } 53 | 54 | const {source, value} = decl; 55 | const line = source.start.line; 56 | const lineContent = getLineContent(line, source.input.css); 57 | 58 | let match = null; 59 | /* eslint-disable no-extra-boolean-cast */ 60 | while (!!(match = PATTERN_URL.exec(value))) { 61 | if (match[1]) { 62 | result.warn(RULENAME, { 63 | node: decl, 64 | ruleName: RULENAME, 65 | line: line, 66 | col: lineContent.indexOf(match[0]) + 1, 67 | message: MSG, 68 | colorMessage: '`' 69 | + lineContent.replace(match[0], chalk.magenta(match[0])) 70 | + '` ' 71 | + chalk.grey(MSG) 72 | }); 73 | global.CSSHINT_INVALID_ALL_COUNT++; 74 | } 75 | } 76 | /* eslint-enable no-extra-boolean-cast */ 77 | }); 78 | } 79 | } 80 | ); 81 | -------------------------------------------------------------------------------- /src/rule/display-property-grouping.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file display-property-grouping 的检测逻辑 3 | * Certain properties shouldn't be used with certain display property values 4 | * https://github.com/CSSLint/csslint/wiki/Beware-of-display-property-grouping-size 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | /** 12 | * 当前文件所代表的规则名称 13 | * 14 | * @const 15 | * @type {string} 16 | */ 17 | const RULENAME = 'display-property-grouping'; 18 | 19 | const propertiesToCheck = { 20 | 'display': 1, 21 | 'float': 'none', 22 | 'height': 1, 23 | 'width': 1, 24 | 'margin': 1, 25 | 'margin-left': 1, 26 | 'margin-right': 1, 27 | 'margin-bottom': 1, 28 | 'margin-top': 1, 29 | 'padding': 1, 30 | 'padding-left': 1, 31 | 'padding-right': 1, 32 | 'padding-bottom': 1, 33 | 'padding-top': 1, 34 | 'vertical-align': 1 35 | }; 36 | 37 | let properties = {}; 38 | 39 | /** 40 | * 对 decl 的处理 41 | * 42 | * @param {Object} result postcss 转换的结果对象 43 | * @param {string} prop 属性名称 44 | * @param {string} display display 属性的值 45 | * @param {string} msg 错误消息要添加的部分 46 | */ 47 | const addWarn = (result, prop, display, msg) => { 48 | const decl = properties[prop]; 49 | if (decl) { 50 | if (typeof propertiesToCheck[prop] !== 'string' 51 | || decl.value.toLowerCase() !== propertiesToCheck[prop] 52 | ) { 53 | const source = decl.source; 54 | const line = source.start.line; 55 | const col = source.start.column; 56 | let str = ''; 57 | let colorStr = ''; 58 | if (msg) { 59 | str = msg + ' can\'t be used with display: `' + display + '`'; 60 | colorStr = msg + ' can\'t be used with display: `' + chalk.magenta(display) + '`'; 61 | } 62 | else { 63 | str = '`' + prop + '` can\'t be used with display: `' + display + '`'; 64 | colorStr = '' 65 | + '`' 66 | + chalk.magenta(prop) 67 | + '` can\'t be used with display: `' 68 | + chalk.magenta(display) + '`'; 69 | } 70 | 71 | result.warn(RULENAME, { 72 | node: decl, 73 | ruleName: RULENAME, 74 | line: line, 75 | col: col, 76 | message: str, 77 | colorMessage: colorStr 78 | }); 79 | global.CSSHINT_INVALID_ALL_COUNT++; 80 | } 81 | } 82 | }; 83 | 84 | /** 85 | * 具体的检测逻辑 86 | * 87 | * @param {Object} opts 参数 88 | * @param {*} opts.ruleVal 当前规则具体配置的值 89 | * @param {string} opts.fileContent 文件内容 90 | * @param {string} opts.filePath 文件路径 91 | */ 92 | export const check = postcss.plugin(RULENAME, opts => 93 | (css, result) => { 94 | if (!opts.ruleVal) { 95 | return; 96 | } 97 | 98 | css.walkRules(rule => { 99 | properties = {}; 100 | 101 | rule.walkDecls(decl => { 102 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 103 | return; 104 | } 105 | 106 | const prop = decl.prop; 107 | if (propertiesToCheck[prop]) { 108 | properties[prop] = decl; 109 | } 110 | }); 111 | 112 | const display = properties.display ? properties.display.value : null; 113 | if (!display) { 114 | return; 115 | } 116 | 117 | switch (display) { 118 | // height, width, margin-top, margin-bottom, float should not be used with inline 119 | case 'inline': 120 | addWarn(result, 'height', display); 121 | addWarn(result, 'width', display); 122 | addWarn(result, 'margin', display); 123 | addWarn(result, 'margin-top', display); 124 | addWarn(result, 'margin-bottom', display); 125 | addWarn(result, 'float', display, 126 | 'display:inline has no effect on floated elements ' 127 | + '(but may be used to fix the IE6 double-margin bug).' 128 | ); 129 | break; 130 | // vertical-align should not be used with block 131 | case 'block': 132 | addWarn(result, 'vertical-align', display); 133 | break; 134 | // float should not be used with inline-block 135 | case 'inline-block': 136 | addWarn(result, 'float', display); 137 | break; 138 | // margin, float should not be used with table 139 | default: 140 | if (display.indexOf('table-') === 0) { 141 | addWarn(result, 'margin', display); 142 | addWarn(result, 'margin-left', display); 143 | addWarn(result, 'margin-right', display); 144 | addWarn(result, 'margin-top', display); 145 | addWarn(result, 'margin-bottom', display); 146 | addWarn(result, 'float', display); 147 | } 148 | } 149 | }); 150 | } 151 | ); 152 | -------------------------------------------------------------------------------- /src/rule/duplicate-background-images.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file duplicate-background-images 的检测逻辑 3 | * Every background-image should be unique. Use a common class for e.g. sprites 4 | * https://github.com/CSSLint/csslint/wiki/Disallow-duplicate-background-images 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | import {getLineContent} from '../util'; 12 | 13 | /** 14 | * 当前文件所代表的规则名称 15 | * 16 | * @const 17 | * @type {string} 18 | */ 19 | const RULENAME = 'duplicate-background-images'; 20 | 21 | /** 22 | * 错误的信息 23 | * 24 | * @const 25 | * @type {string} 26 | */ 27 | const MSG = 'Every background-image should be unique. Use a common class for e.g. sprites'; 28 | 29 | /** 30 | * 匹配 css 中 url 的正则 31 | * 32 | * @const 33 | * @type {RegExp} 34 | */ 35 | const PATTERN_URL = /\burl\s*\((["']?)([^\)]+)\1\)/g; 36 | 37 | let stack = {}; 38 | 39 | /** 40 | * 具体的检测逻辑 41 | * 42 | * @param {Object} opts 参数 43 | * @param {*} opts.ruleVal 当前规则具体配置的值 44 | * @param {string} opts.fileContent 文件内容 45 | * @param {string} opts.filePath 文件路径 46 | */ 47 | export const check = postcss.plugin(RULENAME, opts => 48 | (css, result) => { 49 | 50 | if (!opts.ruleVal) { 51 | return; 52 | } 53 | 54 | stack = {}; 55 | 56 | css.walkDecls(decl => { 57 | 58 | let prop = decl.prop; 59 | if (prop.match(/background/i)) { 60 | const value = decl.value; 61 | let match = null; 62 | /* eslint-disable no-extra-boolean-cast */ 63 | while (!!(match = PATTERN_URL.exec(value))) { 64 | if (typeof stack[match[2]] === 'undefined') { 65 | stack[match[2]] = decl; 66 | } 67 | else { 68 | const str = 'Background image `' 69 | + match[2] 70 | + '` was used multiple times, first declared at line ' 71 | + stack[match[2]].source.start.line 72 | + ', col ' 73 | + stack[match[2]].source.start.column 74 | + '. ' 75 | + MSG; 76 | 77 | const colorStr = 'Background image `' 78 | + chalk.magenta(match[2]) 79 | + '` was used multiple times, first declared at line ' 80 | + stack[match[2]].source.start.line 81 | + ', col ' 82 | + stack[match[2]].source.start.column 83 | + '. ' 84 | + chalk.grey(MSG); 85 | 86 | const source = decl.source; 87 | const line = source.start.line; 88 | const lineContent = getLineContent(line, source.input.css); 89 | const col = lineContent.indexOf(match[2]) + 1; 90 | result.warn(RULENAME, { 91 | node: decl, 92 | ruleName: RULENAME, 93 | line: line, 94 | col: col, 95 | message: str, 96 | colorMessage: colorStr 97 | }); 98 | global.CSSHINT_INVALID_ALL_COUNT++; 99 | } 100 | } 101 | /* eslint-enable no-extra-boolean-cast */ 102 | } 103 | }); 104 | } 105 | ); 106 | -------------------------------------------------------------------------------- /src/rule/duplicate-properties.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file duplicate-properties 的检测逻辑 3 | * Duplicate properties must appear one after the other 4 | * https://github.com/CSSLint/csslint/wiki/Disallow-duplicate-properties 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | import {getLineContent} from '../util'; 12 | 13 | /** 14 | * 当前文件所代表的规则名称 15 | * 16 | * @const 17 | * @type {string} 18 | */ 19 | const RULENAME = 'duplicate-properties'; 20 | 21 | const MSG = 'Duplicate properties must appear one after the other'; 22 | 23 | let properties = {}; 24 | let lastProperty = ''; 25 | 26 | /** 27 | * 具体的检测逻辑 28 | * 29 | * @param {Object} opts 参数 30 | * @param {*} opts.ruleVal 当前规则具体配置的值 31 | * @param {string} opts.fileContent 文件内容 32 | * @param {string} opts.filePath 文件路径 33 | */ 34 | export const check = postcss.plugin(RULENAME, opts => 35 | (css, result) => { 36 | if (!opts.ruleVal) { 37 | return; 38 | } 39 | 40 | css.walkRules(rule => { 41 | properties = {}; 42 | 43 | rule.walkDecls(decl => { 44 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 45 | return; 46 | } 47 | 48 | const {prop, value} = decl; 49 | if (properties[prop] && (lastProperty !== prop || properties[prop] === value)) { 50 | const source = decl.source; 51 | const line = source.start.line; 52 | const lineContent = getLineContent(line, source.input.css); 53 | const col = source.start.column; 54 | result.warn(RULENAME, { 55 | node: decl, 56 | ruleName: RULENAME, 57 | line: line, 58 | col: col, 59 | message: MSG, 60 | colorMessage: '`' 61 | + chalk.magenta(lineContent) 62 | + '` ' 63 | + chalk.grey(MSG) 64 | }); 65 | global.CSSHINT_INVALID_ALL_COUNT++; 66 | } 67 | 68 | properties[prop] = value; 69 | lastProperty = prop; 70 | }); 71 | }); 72 | } 73 | ); 74 | -------------------------------------------------------------------------------- /src/rule/empty-rules.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file empty-rules 的检测逻辑 3 | * Rules without any properties specified should be removed 4 | * https://github.com/CSSLint/csslint/wiki/Disallow-empty-rules 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | import {getLineContent} from '../util'; 12 | 13 | /** 14 | * 当前文件所代表的规则名称 15 | * 16 | * @const 17 | * @type {string} 18 | */ 19 | const RULENAME = 'empty-rules'; 20 | 21 | let propertyCount = 0; 22 | 23 | /** 24 | * 错误的信息 25 | * 26 | * @const 27 | * @type {string} 28 | */ 29 | const MSG = 'Rules without any properties specified should be removed'; 30 | 31 | /** 32 | * 具体的检测逻辑 33 | * 34 | * @param {Object} opts 参数 35 | * @param {*} opts.ruleVal 当前规则具体配置的值 36 | * @param {string} opts.fileContent 文件内容 37 | * @param {string} opts.filePath 文件路径 38 | */ 39 | export const check = postcss.plugin(RULENAME, opts => 40 | (css, result) => { 41 | if (!opts.ruleVal) { 42 | return; 43 | } 44 | 45 | css.walkRules(rule => { 46 | propertyCount = 0; 47 | 48 | rule.walkDecls(() => { 49 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 50 | return; 51 | } 52 | propertyCount++; 53 | }); 54 | 55 | if (propertyCount === 0) { 56 | const source = rule.source; 57 | const line = source.start.line; 58 | const lineContent = getLineContent(line, source.input.css); 59 | const col = source.start.column; 60 | result.warn(RULENAME, { 61 | node: rule, 62 | ruleName: RULENAME, 63 | line: line, 64 | col: col, 65 | message: MSG, 66 | colorMessage: '`' 67 | + lineContent.replace( 68 | rule.selector, 69 | chalk.magenta(rule.selector) 70 | ) 71 | + '` ' 72 | + chalk.grey(MSG) 73 | }); 74 | global.CSSHINT_INVALID_ALL_COUNT++; 75 | } 76 | }); 77 | } 78 | ); 79 | -------------------------------------------------------------------------------- /src/rule/fallback-colors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file fallback-colors 的检测逻辑 3 | * For older browsers that don't support RGBA, HSL, or HSLA, provide a fallback color 4 | * https://github.com/CSSLint/csslint/wiki/Require-fallback-colors 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | import {getPropertyValue} from '../util'; 12 | 13 | /** 14 | * 当前文件所代表的规则名称 15 | * 16 | * @const 17 | * @type {string} 18 | */ 19 | const RULENAME = 'fallback-colors'; 20 | 21 | /* eslint-disable fecs-valid-map-set */ 22 | const propertiesToCheck = { 23 | 'color': 1, 24 | 'background': 1, 25 | 'border-color': 1, 26 | 'border-top-color': 1, 27 | 'border-right-color': 1, 28 | 'border-bottom-color': 1, 29 | 'border-left-color': 1, 30 | 'border': 1, 31 | 'border-top': 1, 32 | 'border-right': 1, 33 | 'border-bottom': 1, 34 | 'border-left': 1, 35 | 'background-color': 1 36 | }; 37 | /* eslint-enable fecs-valid-map-set */ 38 | 39 | let lastProperty; 40 | 41 | /** 42 | * 错误的信息 43 | * 44 | * @const 45 | * @type {string} 46 | */ 47 | const MSG = 'For older browsers that don\'t support RGBA, HSL, or HSLA, provide a fallback color'; 48 | 49 | /** 50 | * decl 的处理 51 | * 52 | * @param {Object} decl postcss 节点对象 53 | * @param {Object} result postcss result 对象 54 | */ 55 | const declHandler = (decl, result) => { 56 | const prop = decl.prop; 57 | const value = getPropertyValue(decl.value); 58 | 59 | const len = value.length; 60 | let i = 0; 61 | let colorType = ''; 62 | 63 | while (i < len) { 64 | if (value[i].type === 'color') { 65 | if ('alpha' in value[i] || 'hue' in value[i]) { 66 | if (/([^\)]+)\(/.test(value[i].text)) { 67 | colorType = RegExp.$1.toUpperCase(); 68 | } 69 | 70 | if (!lastProperty 71 | || (lastProperty.prop !== prop 72 | || lastProperty.colorType !== 'compat') 73 | ) { 74 | const source = decl.source; 75 | const line = source.start.line; 76 | const col = source.start.column; 77 | const str = 'Fallback ' + prop + ' (hex or RGB) should precede ' 78 | + colorType + ' ' + prop; 79 | const colorStr = 'Fallback ' + chalk.magenta(prop) + ' (hex or RGB) should precede ' 80 | + chalk.magenta(colorType) + ' ' + chalk.magenta(prop); 81 | result.warn(RULENAME, { 82 | node: decl, 83 | ruleName: RULENAME, 84 | line: line, 85 | col: col, 86 | message: str + MSG, 87 | colorMessage: '`' 88 | + colorStr 89 | + '` ' 90 | + chalk.grey(MSG) 91 | }); 92 | global.CSSHINT_INVALID_ALL_COUNT++; 93 | } 94 | } 95 | else { 96 | decl.colorType = 'compat'; 97 | } 98 | } 99 | i++; 100 | } 101 | }; 102 | 103 | /** 104 | * 具体的检测逻辑 105 | * 106 | * @param {Object} opts 参数 107 | * @param {*} opts.ruleVal 当前规则具体配置的值 108 | * @param {string} opts.fileContent 文件内容 109 | * @param {string} opts.filePath 文件路径 110 | */ 111 | export const check = postcss.plugin(RULENAME, opts => 112 | (css, result) => { 113 | if (!opts.ruleVal) { 114 | return; 115 | } 116 | 117 | css.walkRules(rule => { 118 | lastProperty = null; 119 | 120 | rule.walkDecls(decl => { 121 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 122 | return; 123 | } 124 | 125 | if (propertiesToCheck[decl.prop]) { 126 | declHandler(decl, result); 127 | } 128 | 129 | lastProperty = decl; 130 | }); 131 | 132 | }); 133 | } 134 | ); 135 | -------------------------------------------------------------------------------- /src/rule/floats.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file floats 的检测逻辑 3 | * Too many floats, you're probably using them for layout. Consider using a grid system instead 4 | * https://github.com/CSSLint/csslint/wiki/Disallow-too-many-floats 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | /** 12 | * 当前文件所代表的规则名称 13 | * 14 | * @const 15 | * @type {string} 16 | */ 17 | const RULENAME = 'floats'; 18 | 19 | /** 20 | * 错误的信息 21 | * 22 | * @const 23 | * @type {string} 24 | */ 25 | const MSG = '`float` must not be greater than '; 26 | 27 | let floatCount = 0; 28 | 29 | /** 30 | * 具体的检测逻辑 31 | * 32 | * @param {Object} opts 参数 33 | * @param {*} opts.ruleVal 当前规则具体配置的值 34 | * @param {string} opts.fileContent 文件内容 35 | * @param {string} opts.filePath 文件路径 36 | */ 37 | export const check = postcss.plugin(RULENAME, opts => 38 | (css, result) => { 39 | if (!opts.ruleVal || isNaN(opts.ruleVal)) { 40 | return; 41 | } 42 | 43 | floatCount = 0; 44 | 45 | css.walkDecls(decl => { 46 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 47 | return; 48 | } 49 | 50 | const {prop, value} = decl; 51 | if (prop === 'float' && value !== 'none') { 52 | floatCount++; 53 | } 54 | }); 55 | 56 | if (floatCount > opts.ruleVal) { 57 | const str = MSG + opts.ruleVal + ', current file `float` is ' + floatCount; 58 | result.warn(RULENAME, { 59 | node: css, 60 | ruleName: RULENAME, 61 | message: str, 62 | colorMessage: chalk.grey(str) 63 | }); 64 | 65 | global.CSSHINT_INVALID_ALL_COUNT++; 66 | } 67 | } 68 | ); 69 | -------------------------------------------------------------------------------- /src/rule/font-face.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file font-face 的检测逻辑 3 | * Too many different web fonts in the same stylesheet 4 | * https://github.com/CSSLint/csslint/wiki/Don't-use-too-many-web-fonts 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | /** 12 | * 当前文件所代表的规则名称 13 | * 14 | * @const 15 | * @type {string} 16 | */ 17 | const RULENAME = 'font-face'; 18 | 19 | /** 20 | * 错误的信息 21 | * 22 | * @const 23 | * @type {string} 24 | */ 25 | const MSG = '@font-face declarations must not be greater than '; 26 | 27 | let fontFaceCount = 0; 28 | 29 | /** 30 | * 具体的检测逻辑 31 | * 32 | * @param {Object} opts 参数 33 | * @param {*} opts.ruleVal 当前规则具体配置的值 34 | * @param {string} opts.fileContent 文件内容 35 | * @param {string} opts.filePath 文件路径 36 | */ 37 | export const check = postcss.plugin(RULENAME, opts => 38 | (css, result) => { 39 | if (!opts.ruleVal || isNaN(opts.ruleVal)) { 40 | return; 41 | } 42 | 43 | fontFaceCount = 0; 44 | 45 | css.walkAtRules(atRule => { 46 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 47 | return; 48 | } 49 | 50 | if (atRule.name === 'font-face') { 51 | fontFaceCount++; 52 | } 53 | }); 54 | 55 | if (fontFaceCount > opts.ruleVal) { 56 | const str = MSG + opts.ruleVal + ', current file @font-face declarations is ' + fontFaceCount; 57 | result.warn(RULENAME, { 58 | node: css, 59 | ruleName: RULENAME, 60 | message: str, 61 | colorMessage: chalk.grey(str) 62 | }); 63 | 64 | global.CSSHINT_INVALID_ALL_COUNT++; 65 | } 66 | } 67 | ); 68 | -------------------------------------------------------------------------------- /src/rule/font-sizes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file font-sizes 的检测逻辑 3 | * Too many font-size declarations, abstraction needed 4 | * https://github.com/CSSLint/csslint/wiki/Don't-use-too-many-font-size-declarations 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | /** 12 | * 当前文件所代表的规则名称 13 | * 14 | * @const 15 | * @type {string} 16 | */ 17 | const RULENAME = 'font-sizes'; 18 | 19 | /** 20 | * 错误的信息 21 | * 22 | * @const 23 | * @type {string} 24 | */ 25 | const MSG = '`font-size` must not be greater than '; 26 | 27 | let fontSizeCount = 0; 28 | 29 | /** 30 | * 具体的检测逻辑 31 | * 32 | * @param {Object} opts 参数 33 | * @param {*} opts.ruleVal 当前规则具体配置的值 34 | * @param {string} opts.fileContent 文件内容 35 | * @param {string} opts.filePath 文件路径 36 | */ 37 | export const check = postcss.plugin(RULENAME, opts => 38 | (css, result) => { 39 | if (!opts.ruleVal || isNaN(opts.ruleVal)) { 40 | return; 41 | } 42 | 43 | fontSizeCount = 0; 44 | 45 | css.walkDecls(decl => { 46 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 47 | return; 48 | } 49 | 50 | const prop = decl.prop; 51 | if (prop === 'font-size') { 52 | fontSizeCount++; 53 | } 54 | }); 55 | 56 | if (fontSizeCount > opts.ruleVal) { 57 | const str = MSG + opts.ruleVal + ', current file `font-size` is ' + fontSizeCount; 58 | result.warn(RULENAME, { 59 | node: css, 60 | ruleName: RULENAME, 61 | message: str, 62 | colorMessage: chalk.grey(str) 63 | }); 64 | 65 | global.CSSHINT_INVALID_ALL_COUNT++; 66 | } 67 | } 68 | ); 69 | -------------------------------------------------------------------------------- /src/rule/gradients.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file gradients 的检测逻辑 3 | * When using a vendor-prefixed gradient, make sure to use them all 4 | * https://github.com/CSSLint/csslint/wiki/Require-all-gradient-definitions 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | import {getLineContent} from '../util'; 12 | 13 | /** 14 | * 当前文件所代表的规则名称 15 | * 16 | * @const 17 | * @type {string} 18 | */ 19 | const RULENAME = 'gradients'; 20 | 21 | /** 22 | * 错误的信息 23 | * 24 | * @const 25 | * @type {string} 26 | */ 27 | const MSG = 'Missing vendor-prefixed CSS gradients for '; 28 | 29 | let gradients = {}; 30 | 31 | /** 32 | * 具体的检测逻辑 33 | * 34 | * @param {Object} opts 参数 35 | * @param {*} opts.ruleVal 当前规则具体配置的值 36 | * @param {string} opts.fileContent 文件内容 37 | * @param {string} opts.filePath 文件路径 38 | */ 39 | export const check = postcss.plugin(RULENAME, opts => 40 | (css, result) => { 41 | if (!opts.ruleVal) { 42 | return; 43 | } 44 | 45 | css.walkRules(rule => { 46 | 47 | gradients = { 48 | moz: 0, 49 | webkit: 0, 50 | oldWebkit: 0, 51 | o: 0 52 | }; 53 | 54 | rule.walkDecls(decl => { 55 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 56 | return; 57 | } 58 | 59 | const value = decl.value; 60 | if (/\-(moz|o|webkit)(?:\-(?:linear|radial))\-gradient/i.test(value)) { 61 | gradients[RegExp.$1] = 1; 62 | } 63 | else if (/\-webkit\-gradient/i.test(value)) { 64 | gradients.oldWebkit = 1; 65 | } 66 | }); 67 | 68 | const missing = []; 69 | 70 | if (!gradients.moz) { 71 | missing.push('Firefox 3.6+: -moz-linear-gradient'); 72 | } 73 | 74 | if (!gradients.webkit) { 75 | missing.push('Webkit (Safari 5+, Chrome): -webkit-linear-gradient'); 76 | } 77 | 78 | if (!gradients.oldWebkit) { 79 | missing.push('Old Webkit (Safari 4+, Chrome): -webkit-gradient'); 80 | } 81 | 82 | if (!gradients.o) { 83 | missing.push('Opera 11.1+: -o-linear-gradient'); 84 | } 85 | 86 | if (missing.length && missing.length < 4) { 87 | const source = rule.source; 88 | const line = source.start.line; 89 | const lineContent = getLineContent(line, source.input.css); 90 | const col = source.start.column; 91 | const str = MSG + missing.join(', '); 92 | result.warn(RULENAME, { 93 | node: rule, 94 | ruleName: RULENAME, 95 | line: line, 96 | col: col, 97 | message: str, 98 | colorMessage: '`' 99 | + lineContent.replace( 100 | rule.selector, 101 | chalk.magenta(rule.selector) 102 | ) 103 | + '` ' 104 | + chalk.grey(str) 105 | }); 106 | global.CSSHINT_INVALID_ALL_COUNT++; 107 | } 108 | }); 109 | } 110 | ); 111 | -------------------------------------------------------------------------------- /src/rule/hex-color.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file hex-color 的检测逻辑 3 | * 029: [强制] RGB颜色值必须使用十六进制记号形式 `#rrggbb`。不允许使用 `rgb()`。 4 | * @author ielgnaw(wuji0223@gmail.com) 5 | */ 6 | 7 | import chalk from 'chalk'; 8 | import postcss from 'postcss'; 9 | 10 | import {getLineContent, changeColorByStartAndEndIndex} from '../util'; 11 | 12 | /** 13 | * 当前文件所代表的规则名称 14 | * 15 | * @const 16 | * @type {string} 17 | */ 18 | const RULENAME = 'hex-color'; 19 | 20 | /** 21 | * 匹配 rgb, hsl 颜色表达式的正则 22 | * 23 | * @const 24 | * @type {RegExp} 25 | */ 26 | const PATTERN_COLOR_EXP = /(\brgb\b|\bhsl\b)/gi; 27 | 28 | /** 29 | * 错误的信息 30 | * 31 | * @const 32 | * @type {string} 33 | */ 34 | const MSG = '' 35 | + 'Color value must use the sixteen hexadecimal mark forms such as `#RGB`.' 36 | + ' Don\'t use RGB、HSL expression'; 37 | 38 | /** 39 | * 具体的检测逻辑 40 | * 41 | * @param {Object} opts 参数 42 | * @param {*} opts.ruleVal 当前规则具体配置的值 43 | * @param {string} opts.fileContent 文件内容 44 | * @param {string} opts.filePath 文件路径 45 | */ 46 | export const check = postcss.plugin(RULENAME, opts => 47 | (css, result) => { 48 | 49 | if (opts.ruleVal) { 50 | 51 | css.walkDecls(decl => { 52 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 53 | return; 54 | } 55 | 56 | let match = null; 57 | /* eslint-disable no-extra-boolean-cast */ 58 | while (!!(match = PATTERN_COLOR_EXP.exec(decl.value))) { 59 | const source = decl.source; 60 | const line = source.start.line; 61 | const lineContent = getLineContent(line, source.input.css); 62 | const col = source.start.column + decl.prop.length + decl.raws.between.length + match.index; 63 | result.warn(RULENAME, { 64 | node: decl, 65 | ruleName: RULENAME, 66 | line: line, 67 | col: col, 68 | message: MSG, 69 | colorMessage: '`' 70 | + changeColorByStartAndEndIndex( 71 | lineContent, col, source.end.column 72 | ) 73 | + '` ' 74 | + chalk.grey(MSG) 75 | }); 76 | global.CSSHINT_INVALID_ALL_COUNT++; 77 | } 78 | /* eslint-enable no-extra-boolean-cast */ 79 | }); 80 | } 81 | } 82 | ); 83 | -------------------------------------------------------------------------------- /src/rule/horizontal-vertical-position.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file horizontal-vertical-position 的检测逻辑 3 | * 033: [强制] 必须同时给出水平和垂直方向的位置。 4 | * @author ielgnaw(wuji0223@gmail.com) 5 | */ 6 | 7 | import chalk from 'chalk'; 8 | import postcss from 'postcss'; 9 | 10 | import {getLineContent, changeColorByStartAndEndIndex} from '../util'; 11 | 12 | /** 13 | * 当前文件所代表的规则名称 14 | * 15 | * @const 16 | * @type {string} 17 | */ 18 | const RULENAME = 'horizontal-vertical-position'; 19 | 20 | /** 21 | * 错误的信息 22 | * 23 | * @const 24 | * @type {string} 25 | */ 26 | const MSG = 'Must give the horizontal and vertical position'; 27 | 28 | /** 29 | * 具体的检测逻辑 30 | * 31 | * @param {Object} opts 参数 32 | * @param {*} opts.ruleVal 当前规则具体配置的值 33 | * @param {string} opts.fileContent 文件内容 34 | * @param {string} opts.filePath 文件路径 35 | */ 36 | export const check = postcss.plugin(RULENAME, opts => 37 | (css, result) => { 38 | 39 | if (opts.ruleVal) { 40 | 41 | css.walkDecls(decl => { 42 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 43 | return; 44 | } 45 | 46 | if (decl.prop === 'background-position') { 47 | const parts = postcss.list.space(decl.value); 48 | if (parts.length < 2) { 49 | const source = decl.source; 50 | const line = source.start.line; 51 | const lineContent = getLineContent(line, source.input.css); 52 | const col = source.start.column + decl.prop.length + decl.raws.between.length; 53 | result.warn(RULENAME, { 54 | node: decl, 55 | ruleName: RULENAME, 56 | line: line, 57 | col: col, 58 | message: MSG, 59 | colorMessage: '`' 60 | + changeColorByStartAndEndIndex( 61 | lineContent, col, source.end.column 62 | ) 63 | + '` ' 64 | + chalk.grey(MSG) 65 | }); 66 | global.CSSHINT_INVALID_ALL_COUNT++; 67 | } 68 | } 69 | }); 70 | } 71 | } 72 | ); 73 | -------------------------------------------------------------------------------- /src/rule/ids.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ids 的检测逻辑 3 | * Selectors should not contain IDs 4 | * https://github.com/CSSLint/csslint/wiki/Disallow-IDs-in-selectors 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | import {getLineContent} from '../util'; 12 | 13 | /** 14 | * 当前文件所代表的规则名称 15 | * 16 | * @const 17 | * @type {string} 18 | */ 19 | const RULENAME = 'ids'; 20 | 21 | /** 22 | * 错误的信息 23 | * 24 | * @const 25 | * @type {string} 26 | */ 27 | const MSG = 'Selectors should not contain IDs'; 28 | 29 | /** 30 | * 具体的检测逻辑 31 | * 32 | * @param {Object} opts 参数 33 | * @param {*} opts.ruleVal 当前规则具体配置的值 34 | * @param {string} opts.fileContent 文件内容 35 | * @param {string} opts.filePath 文件路径 36 | */ 37 | export const check = postcss.plugin(RULENAME, opts => 38 | (css, result) => { 39 | 40 | if (!opts.ruleVal) { 41 | return; 42 | } 43 | 44 | css.walkRules(rule => { 45 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 46 | return; 47 | } 48 | 49 | const {selector, source} = rule; 50 | const selectorGroup = selector.split(','); 51 | let line = source.start.line; 52 | let col = source.start.column; 53 | let lineContent = getLineContent(line, source.input.css); 54 | 55 | for (let i = 0, len = selectorGroup.length; i < len; i++) { 56 | let selectorInGroup = selectorGroup[i] || ''; 57 | // 去掉 attr 选择器 58 | selectorInGroup = selectorInGroup.replace(/\[.+?\](?::[^\s>+~\.#\[]+)?/g, ''); 59 | const match = selectorInGroup.match(/#[^\s>+~\.#\[]+/); 60 | if (match) { 61 | if (selectorInGroup.slice(0, 1) === '\n') { 62 | line = line + 1; 63 | lineContent = getLineContent(line, source.input.css); 64 | col = col + match.index - 1; 65 | } 66 | else { 67 | col = col + match.index; 68 | } 69 | result.warn(RULENAME, { 70 | node: rule, 71 | ruleName: RULENAME, 72 | line: line, 73 | col: col, 74 | message: MSG, 75 | colorMessage: '`' 76 | + lineContent.replace(match[0], chalk.magenta(match[0])) 77 | + '` ' 78 | + chalk.grey(MSG) 79 | }); 80 | } 81 | } 82 | }); 83 | } 84 | ); 85 | -------------------------------------------------------------------------------- /src/rule/import.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file import 的检测逻辑 3 | * Don't use @import, use instead 4 | * https://github.com/CSSLint/csslint/wiki/Disallow-@import 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | import {getLineContent} from '../util'; 12 | 13 | /** 14 | * 当前文件所代表的规则名称 15 | * 16 | * @const 17 | * @type {string} 18 | */ 19 | const RULENAME = 'import'; 20 | 21 | /** 22 | * 错误的信息 23 | * 24 | * @const 25 | * @type {string} 26 | */ 27 | const MSG = 'Don\'t use @import, use instead'; 28 | 29 | /** 30 | * 具体的检测逻辑 31 | * 32 | * @param {Object} opts 参数 33 | * @param {*} opts.ruleVal 当前规则具体配置的值 34 | * @param {string} opts.fileContent 文件内容 35 | * @param {string} opts.filePath 文件路径 36 | */ 37 | export const check = postcss.plugin(RULENAME, opts => 38 | (css, result) => { 39 | 40 | if (!opts.ruleVal) { 41 | return; 42 | } 43 | 44 | css.walkAtRules(atRule => { 45 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 46 | return; 47 | } 48 | 49 | if (atRule.name === 'import') { 50 | const source = atRule.source; 51 | const line = source.start.line; 52 | const lineContent = getLineContent(line, source.input.css); 53 | const col = source.start.column; 54 | result.warn(RULENAME, { 55 | node: atRule, 56 | ruleName: RULENAME, 57 | line: line, 58 | col: col, 59 | message: MSG, 60 | colorMessage: '`' 61 | + lineContent.replace(/@import/g, chalk.magenta('@import')) 62 | + '` ' 63 | + chalk.grey(MSG) 64 | }); 65 | global.CSSHINT_INVALID_ALL_COUNT++; 66 | } 67 | }); 68 | } 69 | ); 70 | -------------------------------------------------------------------------------- /src/rule/leading-zero.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file leading-zero 的检测逻辑 3 | * 025: [强制] 当数值为 0 - 1 之间的小数时,省略整数部分的 `0`。 4 | * @author ielgnaw(wuji0223@gmail.com) 5 | */ 6 | 7 | import chalk from 'chalk'; 8 | import postcss from 'postcss'; 9 | 10 | import {getLineContent, changeColorByIndex} from '../util'; 11 | 12 | /** 13 | * 当前文件所代表的规则名称 14 | * 15 | * @const 16 | * @type {string} 17 | */ 18 | const RULENAME = 'leading-zero'; 19 | 20 | 21 | /** 22 | * 错误的信息 23 | * 24 | * @const 25 | * @type {string} 26 | */ 27 | const MSG = 'When value is between 0 - 1 decimal, omitting the integer part of the `0`'; 28 | 29 | /** 30 | * 具体的检测逻辑 31 | * 32 | * @param {Object} opts 参数 33 | * @param {*} opts.ruleVal 当前规则具体配置的值 34 | * @param {string} opts.fileContent 文件内容 35 | * @param {string} opts.filePath 文件路径 36 | */ 37 | export const check = postcss.plugin(RULENAME, opts => 38 | (css, result) => { 39 | if (opts.ruleVal) { 40 | 41 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 42 | return; 43 | } 44 | 45 | css.walkDecls(decl => { 46 | const parts = postcss.list.space(decl.value); 47 | const source = decl.source; 48 | const lineNum = source.start.line; 49 | 50 | function check(part, startCol) { 51 | const numericVal = parseFloat(part); 52 | if (numericVal < 1 && numericVal > 0 || numericVal < 0 && numericVal > -1) { 53 | if (part.slice(0, 2) === '0.' || part.slice(0, 3) === '-0.') { 54 | const lineContent = getLineContent(lineNum, source.input.css); 55 | const col = lineContent.indexOf(part, startCol); 56 | result.warn(RULENAME, { 57 | node: decl, 58 | ruleName: RULENAME, 59 | line: lineNum, 60 | col: lineContent.indexOf(part) + 1, 61 | message: MSG, 62 | colorMessage: '`' 63 | + changeColorByIndex(lineContent, lineContent.indexOf(part), part) 64 | + '` ' 65 | + chalk.grey(MSG) 66 | }); 67 | } 68 | } 69 | } 70 | 71 | const pattern = /\(([^\)]+)\)/; 72 | for (var i = 0, len = parts.length; i < len; i++) { 73 | var part = parts[i]; 74 | const match = part.match(pattern); 75 | if (match) { 76 | var start = match.index; 77 | match[1].split(/,\s*/).forEach(function (property) { 78 | start = part.indexOf(property, start); 79 | check(property, start); 80 | }); 81 | } 82 | else { 83 | check(part, 0); 84 | } 85 | } 86 | }); 87 | } 88 | } 89 | ); 90 | -------------------------------------------------------------------------------- /src/rule/max-length.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file max-length 的检测逻辑 3 | * 006: [强制] 每行不得超过 `120` 个字符,除非单行不可分割。 4 | * @author ielgnaw(wuji0223@gmail.com) 5 | */ 6 | 7 | import chalk from 'chalk'; 8 | import postcss from 'postcss'; 9 | 10 | /** 11 | * 当前文件所代表的规则名称 12 | * 13 | * @const 14 | * @type {string} 15 | */ 16 | const RULENAME = 'max-length'; 17 | 18 | /** 19 | * 匹配 css 属性值的 url(...); 20 | * 21 | * @const 22 | * @type {RegExp} 23 | */ 24 | const PATTERN_URI = /url\(["']?([^\)"']+)["']?\)/i; 25 | 26 | let excludeLines = []; 27 | 28 | /** 29 | * 具体的检测逻辑 30 | * 31 | * @param {Object} opts 参数 32 | * @param {*} opts.ruleVal 当前规则具体配置的值 33 | * @param {string} opts.fileContent 文件内容 34 | * @param {string} opts.filePath 文件路径 35 | */ 36 | export const check = postcss.plugin(RULENAME, opts => 37 | (css, result) => { 38 | if (opts.ruleVal) { 39 | 40 | excludeLines = []; 41 | 42 | const MSG = 'Each line must not be greater than ' + opts.ruleVal + ' characters'; 43 | 44 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 45 | return; 46 | } 47 | 48 | // 排除掉 background-image: 2px 2px url(data:image/gif;base64,.....); 的情况 49 | css.walkDecls(decl => { 50 | const value = decl.value; 51 | if (PATTERN_URI.test(value)) { 52 | excludeLines.push(decl.source.start.line); 53 | } 54 | }); 55 | 56 | const lines = css.source.input.css.split(/\n/); 57 | 58 | for (let i = 0, len = lines.length; i < len; i++) { 59 | if (lines[i].length > opts.ruleVal 60 | && excludeLines.indexOf(i + 1) === -1 61 | ) { 62 | result.warn(RULENAME, { 63 | node: css, 64 | ruleName: RULENAME, 65 | line: i + 1, 66 | message: MSG, 67 | colorMessage: chalk.grey(MSG) 68 | }); 69 | global.CSSHINT_INVALID_ALL_COUNT++; 70 | } 71 | } 72 | } 73 | } 74 | ); 75 | -------------------------------------------------------------------------------- /src/rule/max-selector-nesting-level.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file max-selector-nesting-level 的检测逻辑 3 | * 014: [建议] 选择器的嵌套层级应不大于 3 级,位置靠后的限定条件应尽可能精确。 4 | * @author ielgnaw(wuji0223@gmail.com) 5 | */ 6 | 7 | import chalk from 'chalk'; 8 | import postcss from 'postcss'; 9 | 10 | import {getLineContent} from '../util'; 11 | 12 | /** 13 | * 当前文件所代表的规则名称 14 | * 15 | * @const 16 | * @type {string} 17 | */ 18 | const RULENAME = 'max-selector-nesting-level'; 19 | 20 | /** 21 | * css 组合的正则匹配 22 | * 23 | * @const 24 | * @type {RegExp} 25 | */ 26 | const PATTERN_COMBINATORS = /[\s>+~]+/g; 27 | 28 | /** 29 | * 获取错误信息 30 | * 31 | * @param {number} level 层级数量 32 | * 33 | * @return {string} 错误信息 34 | */ 35 | const getMsg = level => 36 | '' 37 | + 'A nested hierarchy selector should be no more than ' 38 | + level 39 | + ' levels'; 40 | 41 | /** 42 | * 具体的检测逻辑 43 | * 44 | * @param {Object} opts 参数 45 | * @param {*} opts.ruleVal 当前规则具体配置的值 46 | * @param {string} opts.fileContent 文件内容 47 | * @param {string} opts.filePath 文件路径 48 | */ 49 | export const check = postcss.plugin(RULENAME, opts => 50 | (css, result) => { 51 | 52 | if (!opts.ruleVal || isNaN(opts.ruleVal)) { 53 | return; 54 | } 55 | 56 | const msg = getMsg(opts.ruleVal); 57 | 58 | css.walkRules(rule => { 59 | const selector = rule.selector; 60 | const selectorGroup = selector.split(','); 61 | 62 | for (let i = 0, len = selectorGroup.length; i < len; i++) { 63 | let selectorInGroup = selectorGroup[i] || ''; 64 | 65 | // 去掉 attr 选择器 66 | selectorInGroup = selectorInGroup.replace(/\[.+?\](?::[^\s>+~\.#\[]+)?/g, ''); 67 | 68 | // 先去掉 selectorInGroup 的前后空格,如果有空格,那么 segments 的第一个 item 是空,但是会增加 length 69 | const segments = selectorInGroup.replace(/^[\s\xa0\u3000]+|[\u3000\xa0\s]+$/g, '').split( 70 | PATTERN_COMBINATORS 71 | ); 72 | 73 | if (segments.length > opts.ruleVal) { 74 | const newLineMatch = selectorInGroup.match(/\n/g); 75 | let extraLine = 0; 76 | if (newLineMatch) { 77 | extraLine += newLineMatch.length; 78 | } 79 | 80 | const source = rule.source; 81 | const line = source.start.line + extraLine; 82 | const lineContent = getLineContent(line, source.input.css); 83 | 84 | // 这里去掉 \n 是为了变色 85 | selectorInGroup = selectorInGroup.replace(/\n/g, ''); 86 | 87 | result.warn(RULENAME, { 88 | node: rule, 89 | ruleName: RULENAME, 90 | line: line, 91 | message: msg, 92 | colorMessage: '`' 93 | + lineContent.replace(selectorInGroup, chalk.magenta(selectorInGroup)) 94 | + '` ' 95 | + chalk.grey(msg) 96 | }); 97 | global.CSSHINT_INVALID_ALL_COUNT++; 98 | } 99 | } 100 | }); 101 | } 102 | ); 103 | -------------------------------------------------------------------------------- /src/rule/min-font-size.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file min-font-size 的检测逻辑 3 | * 037: [强制] 需要在 Windows 平台显示的中文内容,其字号应不小于 `12px`。 4 | * @author ielgnaw(wuji0223@gmail.com) 5 | */ 6 | 7 | import chalk from 'chalk'; 8 | import postcss from 'postcss'; 9 | 10 | import {getLineContent, changeColorByStartAndEndIndex} from '../util'; 11 | 12 | /** 13 | * 当前文件所代表的规则名称 14 | * 15 | * @const 16 | * @type {string} 17 | */ 18 | const RULENAME = 'min-font-size'; 19 | 20 | /** 21 | * 数字正则 22 | * 23 | * @const 24 | * @type {RegExp} 25 | */ 26 | const PATTERN_NUMERIC = /^\d+[\.\d]*$/; 27 | 28 | /** 29 | * 错误信息 30 | * 31 | * @const 32 | * @type {string} 33 | */ 34 | const MSG = 'font-size should not be less than '; 35 | 36 | /** 37 | * 具体的检测逻辑 38 | * 39 | * @param {Object} opts 参数 40 | * @param {*} opts.ruleVal 当前规则具体配置的值 41 | * @param {string} opts.fileContent 文件内容 42 | * @param {string} opts.filePath 文件路径 43 | */ 44 | export const check = postcss.plugin(RULENAME, opts => 45 | (css, result) => { 46 | 47 | if (!opts.ruleVal || isNaN(opts.ruleVal)) { 48 | return; 49 | } 50 | 51 | const msgWithVal = MSG + opts.ruleVal + 'px'; 52 | 53 | css.walkDecls(decl => { 54 | 55 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 56 | return; 57 | } 58 | 59 | if (decl.prop === 'font-size') { 60 | if (parseFloat(decl.value) < opts.ruleVal) { 61 | const source = decl.source; 62 | const line = source.start.line; 63 | const lineContent = getLineContent(line, source.input.css); 64 | const val = postcss.list.split(decl.value, 'px')[0]; 65 | if (PATTERN_NUMERIC.test(val)) { 66 | const col = source.start.column + decl.prop.length + decl.raws.between.length; 67 | result.warn(RULENAME, { 68 | node: decl, 69 | ruleName: RULENAME, 70 | line: line, 71 | col: col, 72 | message: msgWithVal, 73 | colorMessage: '`' 74 | + changeColorByStartAndEndIndex( 75 | lineContent, col, source.end.column 76 | ) 77 | + '` ' 78 | + chalk.grey(msgWithVal) 79 | }); 80 | global.CSSHINT_INVALID_ALL_COUNT++; 81 | } 82 | } 83 | } 84 | }); 85 | } 86 | ); 87 | -------------------------------------------------------------------------------- /src/rule/no-bom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file no-bom 的检测逻辑 3 | * 001: [建议] `CSS` 文件使用无 `BOM` 的 `UTF-8` 编码。 4 | * @author ielgnaw(wuji0223@gmail.com) 5 | */ 6 | 7 | import chalk from 'chalk'; 8 | import postcss from 'postcss'; 9 | 10 | /** 11 | * 当前文件所代表的规则名称 12 | * 13 | * @const 14 | * @type {string} 15 | */ 16 | const RULENAME = 'no-bom'; 17 | 18 | /** 19 | * 错误的信息 20 | * 21 | * @const 22 | * @type {string} 23 | */ 24 | const MSG = 'CSS file should using UTF-8 coding without BOM'; 25 | 26 | /** 27 | * 具体的检测逻辑 28 | * 29 | * @param {Object} opts 参数 30 | * @param {*} opts.ruleVal 当前规则具体配置的值 31 | * @param {string} opts.fileContent 文件内容 32 | * @param {string} opts.filePath 文件路径 33 | */ 34 | export const check = postcss.plugin(RULENAME, opts => 35 | (css, result) => { 36 | if (opts.ruleVal) { 37 | 38 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 39 | return; 40 | } 41 | 42 | const bufContent = new Buffer(opts.fileContent, 'utf8'); 43 | 44 | const hasBOM 45 | = (bufContent[0] === 0xEF && bufContent[1] === 0xBB && bufContent[2] === 0xBF) // UTF-8 +BOM 46 | || (bufContent[0] === 0xEF && bufContent[1] === 0xBF && bufContent[2] === 0xBD); // unicode UTF16 LE 47 | 48 | if (hasBOM) { 49 | result.warn(RULENAME, { 50 | node: css, 51 | ruleName: RULENAME, 52 | message: MSG, 53 | colorMessage: chalk.grey(MSG) 54 | }); 55 | global.CSSHINT_INVALID_ALL_COUNT++; 56 | } 57 | } 58 | } 59 | ); 60 | -------------------------------------------------------------------------------- /src/rule/omit-protocol-in-url.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file omit-protocol-in-url 的检测逻辑 3 | * 027: [建议] `url()` 函数中的绝对路径可省去协议名。 4 | * @author ielgnaw(wuji0223@gmail.com) 5 | */ 6 | 7 | import chalk from 'chalk'; 8 | import postcss from 'postcss'; 9 | 10 | import {getLineContent, changeColorByIndex} from '../util'; 11 | 12 | /** 13 | * 当前文件所代表的规则名称 14 | * 15 | * @const 16 | * @type {string} 17 | */ 18 | const RULENAME = 'omit-protocol-in-url'; 19 | 20 | /** 21 | * 匹配 css 中 url 的正则 22 | * 23 | * @const 24 | * @type {RegExp} 25 | */ 26 | const PATTERN_URL = /\burl\s*\((["']?)([^\)]+)\1\)/g; 27 | 28 | /** 29 | * 匹配 url() 中 path 的协议 30 | * 31 | * @const 32 | * @type {RegExp} 33 | */ 34 | const PATTERN_PROTOCOL = /^((https?|s?ftp|irc[6s]?|git|afp|telnet|smb):\/\/){1}/gi; 35 | 36 | /** 37 | * 错误信息 38 | * 39 | * @const 40 | * @type {string} 41 | */ 42 | const MSG = 'Path in the `url()` should remove protocol'; 43 | 44 | /** 45 | * 具体的检测逻辑 46 | * 47 | * @param {Object} opts 参数 48 | * @param {*} opts.ruleVal 当前规则具体配置的值 49 | * @param {string} opts.fileContent 文件内容 50 | * @param {string} opts.filePath 文件路径 51 | */ 52 | export const check = postcss.plugin(RULENAME, opts => 53 | (css, result) => { 54 | if (opts.ruleVal) { 55 | 56 | css.walkDecls(decl => { 57 | 58 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 59 | return; 60 | } 61 | 62 | const {source, value} = decl; 63 | const line = source.start.line; 64 | const lineContent = getLineContent(line, source.input.css); 65 | 66 | let match = null; 67 | let matchProtocol = null; 68 | 69 | /* eslint-disable no-extra-boolean-cast */ 70 | while (!!(match = PATTERN_URL.exec(value))) { 71 | const url = match[2]; 72 | 73 | // decl.value 相对于 lineContent 的 index 74 | const valueIndex = lineContent.indexOf(decl.value); 75 | 76 | // 相对于 decl.value 的 index 77 | const index = valueIndex + match.input.indexOf(url); 78 | while (!!(matchProtocol = PATTERN_PROTOCOL.exec(url))) { 79 | result.warn(RULENAME, { 80 | node: decl, 81 | ruleName: RULENAME, 82 | line: line, 83 | col: index + 1, 84 | message: MSG, 85 | colorMessage: '`' 86 | + changeColorByIndex(lineContent, index, matchProtocol[0]) 87 | + '` ' 88 | + chalk.grey(MSG) 89 | }); 90 | global.CSSHINT_INVALID_ALL_COUNT++; 91 | } 92 | } 93 | /* eslint-enable no-extra-boolean-cast */ 94 | }); 95 | } 96 | } 97 | ); 98 | -------------------------------------------------------------------------------- /src/rule/outline-none.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file outline-none 的检测逻辑 3 | * Use of outline: none or outline: 0 should be limited to :focus rules 4 | * https://github.com/CSSLint/csslint/wiki/Disallow-outline:none 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | import {getLineContent} from '../util'; 12 | 13 | /** 14 | * 当前文件所代表的规则名称 15 | * 16 | * @const 17 | * @type {string} 18 | */ 19 | const RULENAME = 'outline-none'; 20 | 21 | /** 22 | * 错误信息 23 | * 24 | * @const 25 | * @type {string} 26 | */ 27 | const MSG1 = 'Outlines should only be modified using :focus'; 28 | 29 | /** 30 | * 错误信息 31 | * 32 | * @const 33 | * @type {string} 34 | */ 35 | const MSG2 = 'Outlines shouldn\'t be hidden unless other visual changes are made'; 36 | 37 | let lastRule; 38 | 39 | /** 40 | * 具体的检测逻辑 41 | * 42 | * @param {Object} opts 参数 43 | * @param {*} opts.ruleVal 当前规则具体配置的值 44 | * @param {string} opts.fileContent 文件内容 45 | * @param {string} opts.filePath 文件路径 46 | */ 47 | export const check = postcss.plugin(RULENAME, opts => 48 | (css, result) => { 49 | 50 | if (!opts.ruleVal) { 51 | return; 52 | } 53 | 54 | css.walkRules(rule => { 55 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 56 | return; 57 | } 58 | 59 | const selector = rule.selector; 60 | if (selector) { 61 | lastRule = { 62 | rule: rule, 63 | selector: selector, 64 | propCount: 0, 65 | outline: false 66 | }; 67 | } 68 | else { 69 | lastRule = null; 70 | } 71 | 72 | rule.walkDecls(decl => { 73 | const {prop, value} = decl; 74 | if (lastRule) { 75 | lastRule.propCount++; 76 | if (prop === 'outline' && (value === 'none' || value.toString() === '0')) { 77 | lastRule.outline = true; 78 | } 79 | } 80 | }); 81 | 82 | if (lastRule) { 83 | if (lastRule.outline) { 84 | const source = lastRule.rule.source; 85 | const line = source.start.line; 86 | const col = source.start.column; 87 | const lineContent = getLineContent(line, source.input.css); 88 | if (lastRule.selector.toLowerCase().indexOf(':focus') === -1) { 89 | result.warn(RULENAME, { 90 | node: lastRule.rule, 91 | ruleName: RULENAME, 92 | line: line, 93 | col: col, 94 | message: MSG1, 95 | colorMessage: '`' 96 | + lineContent.replace(selector, chalk.magenta(selector)) 97 | + '` ' 98 | + chalk.grey(MSG1) 99 | }); 100 | 101 | global.CSSHINT_INVALID_ALL_COUNT++; 102 | } 103 | else if (lastRule.propCount === 1) { 104 | result.warn(RULENAME, { 105 | node: lastRule.rule, 106 | ruleName: RULENAME, 107 | line: line, 108 | col: col, 109 | message: MSG2, 110 | colorMessage: '`' 111 | + lineContent.replace(selector, chalk.magenta(selector)) 112 | + '` ' 113 | + chalk.grey(MSG2) 114 | }); 115 | 116 | global.CSSHINT_INVALID_ALL_COUNT++; 117 | } 118 | } 119 | } 120 | }); 121 | } 122 | ); 123 | -------------------------------------------------------------------------------- /src/rule/property-not-existed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file property-not-existed 的检测逻辑,检测属性是否存在 3 | * @author ielgnaw(wuji0223@gmail.com) 4 | */ 5 | 6 | import chalk from 'chalk'; 7 | import postcss from 'postcss'; 8 | 9 | import {getPrefixList} from '../prefixes'; 10 | 11 | const prefixList = getPrefixList(); 12 | 13 | /** 14 | * 当前文件所代表的规则名称 15 | * 16 | * @const 17 | * @type {string} 18 | */ 19 | const RULENAME = 'property-not-existed'; 20 | 21 | /** 22 | * 具体的检测逻辑 23 | * 24 | * @param {Object} opts 参数 25 | * @param {*} opts.ruleVal 当前规则具体配置的值 26 | * @param {string} opts.fileContent 文件内容 27 | * @param {string} opts.filePath 文件路径 28 | */ 29 | export const check = postcss.plugin(RULENAME, opts => 30 | (css, result) => { 31 | 32 | if (opts.ruleVal) { 33 | 34 | css.walkDecls(decl => { 35 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 36 | return; 37 | } 38 | 39 | const prop = decl.prop; 40 | const standardProperty = prop.replace(/^\-(webkit|moz|ms|o)\-/g, ''); 41 | // 标准模式在 prefixList 中,那么如果 propertyName 不在 prefixList 中 42 | // 即这个属性用错了,例如 -o-animation 43 | if (prefixList.indexOf(standardProperty) > -1) { 44 | if (prefixList.indexOf(prop) <= -1) { 45 | 46 | const source = decl.source; 47 | const line = source.start.line; 48 | const col = source.start.column; 49 | 50 | result.warn(RULENAME, { 51 | node: decl, 52 | ruleName: RULENAME, 53 | line: line, 54 | col: col, 55 | message: '' 56 | + 'Current property ' 57 | + '`' 58 | + prop 59 | + '` ' 60 | + 'is not existed', 61 | colorMessage: '' 62 | + 'Current property ' 63 | + '`' 64 | + chalk.magenta(prop) 65 | + '` ' 66 | + 'is not existed' 67 | }); 68 | global.CSSHINT_INVALID_ALL_COUNT++; 69 | } 70 | } 71 | }); 72 | } 73 | } 74 | ); 75 | -------------------------------------------------------------------------------- /src/rule/qualified-headings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file qualified-headings 的检测逻辑 3 | * Headings should not be qualified 4 | * https://github.com/CSSLint/csslint/wiki/Disallow-qualified-headings 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | import {getLineContent} from '../util'; 12 | 13 | /** 14 | * 当前文件所代表的规则名称 15 | * 16 | * @const 17 | * @type {string} 18 | */ 19 | const RULENAME = 'qualified-headings'; 20 | 21 | /** 22 | * 错误的信息 23 | * 24 | * @const 25 | * @type {string} 26 | */ 27 | const MSG = 'Headings should not be qualified (namespaced)'; 28 | 29 | /** 30 | * css 组合的正则匹配 31 | * 32 | * @const 33 | * @type {RegExp} 34 | */ 35 | const PATTERN_COMBINATORS = /[\s>+~]+/g; 36 | 37 | /** 38 | * 具体的检测逻辑 39 | * 40 | * @param {Object} opts 参数 41 | * @param {*} opts.ruleVal 当前规则具体配置的值 42 | * @param {string} opts.fileContent 文件内容 43 | * @param {string} opts.filePath 文件路径 44 | */ 45 | export const check = postcss.plugin(RULENAME, opts => 46 | (css, result) => { 47 | 48 | if (!opts.ruleVal) { 49 | return; 50 | } 51 | 52 | css.walkRules(rule => { 53 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 54 | return; 55 | } 56 | 57 | const {selector, source} = rule; 58 | const selectorGroup = selector.split(','); 59 | let line = source.start.line; 60 | const col = source.start.column; 61 | let lineContent = getLineContent(line, source.input.css); 62 | 63 | for (let i = 0, len = selectorGroup.length; i < len; i++) { 64 | const selectorInGroup = selectorGroup[i] || ''; 65 | const segments = selectorInGroup.split(PATTERN_COMBINATORS); 66 | 67 | // 跳过第一个,第一个是 h[1-6] 是合法的 68 | for (let j = 1, segmentLen = segments.length; j < segmentLen; j++) { 69 | const segment = segments[j]; 70 | if (/h[1-6]/.test(segment)) { 71 | if (selectorInGroup.slice(0, 1) === '\n') { 72 | line = line + 1; 73 | lineContent = getLineContent(line, source.input.css); 74 | } 75 | result.warn(RULENAME, { 76 | node: rule, 77 | ruleName: RULENAME, 78 | line: line, 79 | col: col + lineContent.indexOf(segment), 80 | message: MSG, 81 | colorMessage: '`' 82 | + lineContent.replace(segment, chalk.magenta(segment)) 83 | + '` ' 84 | + chalk.grey(MSG) 85 | }); 86 | 87 | global.CSSHINT_INVALID_ALL_COUNT++; 88 | } 89 | } 90 | } 91 | }); 92 | } 93 | ); 94 | -------------------------------------------------------------------------------- /src/rule/regex-selectors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file regex-selectors 的检测逻辑 3 | * Selectors that look like regular expressions are slow and should be avoided 4 | * https://github.com/CSSLint/csslint/wiki/Disallow-selectors-that-look-like-regular-expressions 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | import {getLineContent} from '../util'; 12 | 13 | /** 14 | * 当前文件所代表的规则名称 15 | * 16 | * @const 17 | * @type {string} 18 | */ 19 | const RULENAME = 'regex-selectors'; 20 | 21 | /** 22 | * 错误的信息 23 | * 24 | * @const 25 | * @type {string} 26 | */ 27 | const MSG = 'Selectors that look like regular expressions are slow and should be avoided'; 28 | 29 | /** 30 | * 具体的检测逻辑 31 | * 32 | * @param {Object} opts 参数 33 | * @param {*} opts.ruleVal 当前规则具体配置的值 34 | * @param {string} opts.fileContent 文件内容 35 | * @param {string} opts.filePath 文件路径 36 | */ 37 | export const check = postcss.plugin(RULENAME, opts => 38 | (css, result) => { 39 | 40 | if (opts.ruleVal) { 41 | 42 | css.walkRules(rule => { 43 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 44 | return; 45 | } 46 | 47 | const {selector, source} = rule; 48 | 49 | const selectorGroup = selector.split(','); 50 | let line = source.start.line; 51 | let lineContent = getLineContent(line, source.input.css); 52 | 53 | for (let i = 0, len = selectorGroup.length; i < len; i++) { 54 | const selectorInGroup = selectorGroup[i] || ''; 55 | const attrs = selectorInGroup.match(/\[.+?\](?::[^\s>+~\.#\[]+)?/g); 56 | if (!attrs) { 57 | continue; 58 | } 59 | 60 | if (selectorInGroup.slice(0, 1) === '\n') { 61 | line = line + 1; 62 | lineContent = getLineContent(line, source.input.css); 63 | } 64 | 65 | for (let j = 0, attrsLen = attrs.length; j < attrsLen; j++) { 66 | const attr = attrs[j]; 67 | if (/([\~\|\^\$\*]=)/.test(attr)) { 68 | const col = lineContent.indexOf(attr) + 1; 69 | result.warn(RULENAME, { 70 | node: rule, 71 | ruleName: RULENAME, 72 | line: line, 73 | col: col, 74 | message: MSG, 75 | colorMessage: '`' 76 | + lineContent.replace(attr, chalk.magenta(attr)) 77 | + '` ' 78 | + chalk.grey(MSG) 79 | }); 80 | 81 | global.CSSHINT_INVALID_ALL_COUNT++; 82 | } 83 | } 84 | 85 | } 86 | }); 87 | } 88 | } 89 | ); 90 | -------------------------------------------------------------------------------- /src/rule/require-after-space.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file require-after-space 的检测逻辑 3 | * `:` 对应 004: [强制] `属性名` 与之后的 `:` 之间不允许包含空格, `:` 与 `属性值` 之间必须包含空格。 4 | * `,` 对应 005: [强制] `列表型属性值` 书写在单行时,`,` 后必须跟一个空格。 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | import {getLineContent} from '../util'; 12 | 13 | /** 14 | * 当前文件所代表的规则名称 15 | * 16 | * @const 17 | * @type {string} 18 | */ 19 | const RULENAME = 'require-after-space'; 20 | 21 | /** 22 | * 冒号 23 | * 24 | * @const 25 | * @type {string} 26 | */ 27 | const COLON = ':'; 28 | 29 | /** 30 | * 逗号 31 | * 32 | * @const 33 | * @type {string} 34 | */ 35 | const COMMA = ','; 36 | 37 | /** 38 | * 匹配 css 属性值的 url(...); 39 | * 40 | * @const 41 | * @type {RegExp} 42 | */ 43 | const PATTERN_URI = /url\(["']?([^\)"']+)["']?\)/i; 44 | 45 | /** 46 | * 冒号的错误信息 47 | * 48 | * @const 49 | * @type {string} 50 | */ 51 | const COLON_MSG = '' 52 | + 'Disallow contain spaces between the `attr-name` and `:`, ' 53 | + 'Must contain spaces between `:` and `attr-value`'; 54 | 55 | /** 56 | * 逗号的错误信息 57 | * 58 | * @const 59 | * @type {string} 60 | */ 61 | const COMMA_MSG = 'Must contain spaces after `,` in `attr-value`'; 62 | 63 | const arrayProto = Array.prototype; 64 | 65 | /** 66 | * 具体的检测逻辑 67 | * 68 | * @param {Object} opts 参数 69 | * @param {*} opts.ruleVal 当前规则具体配置的值 70 | * @param {string} opts.fileContent 文件内容 71 | * @param {string} opts.filePath 文件路径 72 | */ 73 | export const check = postcss.plugin(RULENAME, opts => 74 | (css, result) => { 75 | const ruleVal = opts.ruleVal; 76 | const realRuleVal = []; 77 | arrayProto.push[Array.isArray(ruleVal) ? 'apply' : 'call'](realRuleVal, ruleVal); 78 | 79 | if (realRuleVal.length) { 80 | 81 | css.walkDecls(decl => { 82 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 83 | return; 84 | } 85 | 86 | const source = decl.source; 87 | const line = source.start.line; 88 | const lineContent = getLineContent(line, source.input.css) || ''; 89 | 90 | if (realRuleVal.indexOf(COLON) !== -1) { 91 | const between = decl.raws.between; 92 | 93 | if (between.slice(0, 1) !== ':' // `属性名` 与之后的 `:` 之间包含空格了 94 | || between.slice(-1) === ':' // `:` 与 `属性值` 之间不包含空格 95 | ) { 96 | const colorStr = decl.prop + decl.raws.between + decl.value; 97 | result.warn(RULENAME, { 98 | node: decl, 99 | ruleName: RULENAME, 100 | errorChar: COLON, 101 | line: line, 102 | message: COLON_MSG, 103 | colorMessage: '`' 104 | + lineContent.replace( 105 | colorStr, 106 | chalk.magenta(colorStr) 107 | ) 108 | + '` ' 109 | + chalk.grey(COLON_MSG) 110 | }); 111 | global.CSSHINT_INVALID_ALL_COUNT++; 112 | } 113 | } 114 | 115 | if (realRuleVal.indexOf(COMMA) !== -1) { 116 | 117 | const value = decl.value; 118 | 119 | // 排除掉 uri 的情况,例如 120 | // background-image: url(data:image/gif;base64,R0lGODlhCAAHAIABAGZmZv...); 121 | // background-image: 2px 2px url(data:image/gif;base64,R0lGODlhCAAHAIABAGZmZv...); 122 | // background-image: url(data:image/gif;base64,R0lGODlhCAAHAIABAGZmZv...) 2px 2px; 123 | if (!PATTERN_URI.test(value)) { 124 | const items = lineContent.split(';'); 125 | for (let j = 0, jLen = items.length; j < jLen; j++) { 126 | const s = items[j]; 127 | if (s.indexOf(',') > -1 128 | && /.*,(?!\s)/.test(s) 129 | && s.length !== lineContent.length // s.length === lineContent.length 的情况表示当前行结束了 130 | ) { 131 | result.warn(RULENAME, { 132 | node: decl, 133 | ruleName: RULENAME, 134 | errorChar: COMMA, 135 | line: line, 136 | message: COMMA_MSG, 137 | colorMessage: '`' 138 | + lineContent.replace( 139 | value, 140 | chalk.magenta(value) 141 | ) 142 | + '` ' 143 | + chalk.grey(COMMA_MSG) 144 | }); 145 | global.CSSHINT_INVALID_ALL_COUNT++; 146 | } 147 | } 148 | } 149 | } 150 | 151 | }); 152 | } 153 | } 154 | ); 155 | -------------------------------------------------------------------------------- /src/rule/require-around-space.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file require-around-space 的检测逻辑 3 | * `>`, `+`, `~` 对应 009: [强制] `>`、`+`、`~` 选择器的两边各保留一个空格。 4 | * @author ielgnaw(wuji0223@gmail.com) 5 | */ 6 | 7 | import chalk from 'chalk'; 8 | import postcss from 'postcss'; 9 | 10 | import {getLineContent, trim} from '../util'; 11 | 12 | /** 13 | * 当前文件所代表的规则名称 14 | * 15 | * @const 16 | * @type {string} 17 | */ 18 | const RULENAME = 'require-around-space'; 19 | 20 | /** 21 | * css 组合的正则匹配 22 | * 23 | * @const 24 | * @type {RegExp} 25 | */ 26 | const PATTERN_COMBINATORS = /[^\s>+~=]+/g; // 排除 ~=, +=, >= 27 | 28 | /** 29 | * 获取错误信息 30 | * 31 | * @param {string} combinator 组合的字符 32 | * 33 | * @return {string} 错误信息 34 | */ 35 | const getMsg = combinator => 36 | '' 37 | + 'Around the `' 38 | + combinator 39 | + '` selector will keep a space'; 40 | 41 | const arrayProto = Array.prototype; 42 | 43 | /** 44 | * 具体的检测逻辑 45 | * 46 | * @param {Object} opts 参数 47 | * @param {*} opts.ruleVal 当前规则具体配置的值 48 | * @param {string} opts.fileContent 文件内容 49 | * @param {string} opts.filePath 文件路径 50 | */ 51 | export const check = postcss.plugin(RULENAME, opts => 52 | (css, result) => { 53 | 54 | const ruleVal = opts.ruleVal; 55 | const realRuleVal = []; 56 | arrayProto.push[Array.isArray(ruleVal) ? 'apply' : 'call'](realRuleVal, ruleVal); 57 | 58 | if (realRuleVal.length) { 59 | 60 | const invalidList = []; 61 | css.walkRules(rule => { 62 | /* jshint maxcomplexity: 11 */ 63 | let selector = rule.selector; 64 | 65 | // 排除掉 .aaa:nth-child(4n+1) 这样的选择器 66 | selector = selector.replace(/\([\s\S]*?\)/g, ''); 67 | 68 | const segments = selector.split(PATTERN_COMBINATORS); 69 | const len = segments.length; 70 | 71 | for (let i = 0; i < len; i++) { 72 | let segment = segments[i]; 73 | 74 | if (!segment) { 75 | continue; 76 | } 77 | 78 | const lastChar = segment.slice(-1); 79 | const firstChar = segment.slice(0, 1); 80 | if (segment) { 81 | segment = trim(segment); 82 | if (realRuleVal.indexOf(segment) <= -1) { 83 | continue; 84 | } 85 | 86 | if (i === 0) { 87 | if (lastChar !== ' ') { 88 | invalidList.push({ 89 | invalidChar: segment, 90 | rule: rule 91 | }); 92 | continue; 93 | } 94 | } 95 | else if (i === len - 1) { 96 | if (firstChar !== ' ') { 97 | invalidList.push({ 98 | invalidChar: segment, 99 | rule: rule 100 | }); 101 | continue; 102 | } 103 | } 104 | else { 105 | if (lastChar !== ' ' || firstChar !== ' ') { 106 | invalidList.push({ 107 | invalidChar: segment, 108 | rule: rule 109 | }); 110 | continue; 111 | } 112 | } 113 | } 114 | } 115 | }); 116 | 117 | invalidList.forEach(invalidRule => { 118 | const {invalidChar, rule} = invalidRule; 119 | const msg = getMsg(invalidRule.invalidChar); 120 | const source = rule.source; 121 | const line = source.start.line; 122 | const lineContent = getLineContent(line, source.input.css); 123 | const col = lineContent.indexOf(invalidChar); 124 | result.warn(RULENAME, { 125 | node: rule, 126 | ruleName: RULENAME, 127 | errorChar: invalidChar, 128 | line: line, 129 | col: col + 1, 130 | message: msg, 131 | colorMessage: '`' 132 | + lineContent.replace(invalidChar, chalk.magenta(invalidChar)) 133 | + '` ' 134 | + chalk.grey(msg) 135 | }); 136 | global.CSSHINT_INVALID_ALL_COUNT++; 137 | }); 138 | } 139 | } 140 | ); 141 | -------------------------------------------------------------------------------- /src/rule/require-before-space.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file require-before-space 的检测逻辑 3 | * `{` 对应 003: [强制] `选择器` 与 `{` 之间必须包含空格。 4 | * @author ielgnaw(wuji0223@gmail.com) 5 | */ 6 | 7 | import chalk from 'chalk'; 8 | import postcss from 'postcss'; 9 | 10 | import {getLineContent} from '../util'; 11 | 12 | /** 13 | * 当前文件所代表的规则名称 14 | * 15 | * @const 16 | * @type {string} 17 | */ 18 | const RULENAME = 'require-before-space'; 19 | 20 | /** 21 | * 错误的信息 22 | * 23 | * @const 24 | * @type {string} 25 | */ 26 | const MSG = 'Must contain spaces before the `{`'; 27 | 28 | const arrayProto = Array.prototype; 29 | 30 | /** 31 | * 具体的检测逻辑 32 | * 33 | * @param {Object} opts 参数 34 | * @param {*} opts.ruleVal 当前规则具体配置的值 35 | * @param {string} opts.fileContent 文件内容 36 | * @param {string} opts.filePath 文件路径 37 | */ 38 | export const check = postcss.plugin(RULENAME, opts => 39 | (css, result) => { 40 | const ruleVal = opts.ruleVal; 41 | const realRuleVal = []; 42 | arrayProto.push[Array.isArray(ruleVal) ? 'apply' : 'call'](realRuleVal, ruleVal); 43 | 44 | if (realRuleVal.length) { 45 | css.walkRules(rule => { 46 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 47 | return; 48 | } 49 | 50 | // 只有 { 时,才能用 between 处理,其他符号的 require-before-space 规则还未实现 51 | if (rule.raws.between === '' && realRuleVal.indexOf('{') !== -1) { 52 | const source = rule.source; 53 | const line = source.start.line; 54 | const col = source.start.column + rule.selector.length; 55 | const lineContent = getLineContent(line, source.input.css) || ''; 56 | result.warn(RULENAME, { 57 | node: rule, 58 | ruleName: RULENAME, 59 | errorChar: '{', 60 | line: line, 61 | col: col, 62 | message: MSG, 63 | colorMessage: '`' 64 | + lineContent.replace( 65 | '{', 66 | chalk.magenta('{') 67 | ) 68 | + '` ' 69 | + chalk.grey(MSG) 70 | }); 71 | global.CSSHINT_INVALID_ALL_COUNT++; 72 | } 73 | }); 74 | } 75 | } 76 | ); 77 | -------------------------------------------------------------------------------- /src/rule/require-number.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file require-number 的检测逻辑 3 | * `font-weight` 对应 039: [强制] `font-weight` 属性必须使用数值方式描述。 4 | * `line-height` 对应 040: [建议] `line-height` 在定义文本段落时,应使用数值。 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | import {getLineContent, changeColorByStartAndEndIndex} from '../util'; 12 | 13 | /** 14 | * 当前文件所代表的规则名称 15 | * 16 | * @const 17 | * @type {string} 18 | */ 19 | const RULENAME = 'require-number'; 20 | 21 | const PATTERN_NUMERIC = /^\d*[\.\d%]*$/; 22 | 23 | /** 24 | * 错误的信息 25 | * 26 | * @const 27 | * @type {string} 28 | */ 29 | const MSG = ' must be a number value'; 30 | 31 | const arrayProto = Array.prototype; 32 | 33 | /** 34 | * 具体的检测逻辑 35 | * 36 | * @param {Object} opts 参数 37 | * @param {*} opts.ruleVal 当前规则具体配置的值 38 | * @param {string} opts.fileContent 文件内容 39 | * @param {string} opts.filePath 文件路径 40 | */ 41 | export const check = postcss.plugin(RULENAME, opts => 42 | (css, result) => { 43 | 44 | const ruleVal = opts.ruleVal; 45 | const realRuleVal = []; 46 | arrayProto.push[Array.isArray(ruleVal) ? 'apply' : 'call'](realRuleVal, ruleVal); 47 | 48 | if (realRuleVal.length) { 49 | 50 | css.walkDecls(decl => { 51 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 52 | return; 53 | } 54 | 55 | const prop = decl.prop; 56 | 57 | if (realRuleVal.indexOf(prop) !== -1) { 58 | if (!PATTERN_NUMERIC.test(decl.value)) { 59 | const source = decl.source; 60 | const line = source.start.line; 61 | const lineContent = getLineContent(line, source.input.css); 62 | const col = source.start.column + decl.prop.length + decl.raws.between.length; 63 | result.warn(RULENAME, { 64 | node: decl, 65 | ruleName: RULENAME, 66 | errorChar: prop, 67 | line: line, 68 | col: col, 69 | message: prop + MSG, 70 | colorMessage: '`' 71 | + changeColorByStartAndEndIndex( 72 | lineContent, col, source.end.column 73 | ) 74 | + '` ' 75 | + chalk.grey(prop + MSG) 76 | }); 77 | global.CSSHINT_INVALID_ALL_COUNT++; 78 | } 79 | } 80 | 81 | }); 82 | } 83 | } 84 | ); 85 | -------------------------------------------------------------------------------- /src/rule/require-transition-property.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file require-transition-property 的检测逻辑 3 | * 041: [强制] 使用 `transition` 时应指定 `transition-property`。 4 | * @author ielgnaw(wuji0223@gmail.com) 5 | */ 6 | 7 | import chalk from 'chalk'; 8 | import postcss from 'postcss'; 9 | 10 | import {getLineContent} from '../util'; 11 | 12 | /** 13 | * 当前文件所代表的规则名称 14 | * 15 | * @const 16 | * @type {string} 17 | */ 18 | const RULENAME = 'require-transition-property'; 19 | 20 | /** 21 | * 错误的信息 22 | * 23 | * @const 24 | * @type {string} 25 | */ 26 | const MSG = 'When using the `transition`, `transition-property` should be specified'; 27 | 28 | /** 29 | * 具体的检测逻辑 30 | * 31 | * @param {Object} opts 参数 32 | * @param {*} opts.ruleVal 当前规则具体配置的值 33 | * @param {string} opts.fileContent 文件内容 34 | * @param {string} opts.filePath 文件路径 35 | */ 36 | export const check = postcss.plugin(RULENAME, opts => 37 | (css, result) => { 38 | 39 | if (opts.ruleVal) { 40 | 41 | css.walkDecls(decl => { 42 | 43 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 44 | return; 45 | } 46 | 47 | const prop = decl.prop; 48 | 49 | if (prop === 'transition') { 50 | const parts = postcss.list.space(decl.value); 51 | if (parts.indexOf('all') > -1) { 52 | const source = decl.source; 53 | const line = source.start.line; 54 | const lineContent = getLineContent(line, source.input.css); 55 | result.warn(RULENAME, { 56 | node: decl, 57 | ruleName: RULENAME, 58 | line: line, 59 | message: MSG, 60 | colorMessage: '`' 61 | + lineContent.replace(/\ball\b/g, chalk.magenta('all')) 62 | + '` ' 63 | + chalk.grey(MSG) 64 | }); 65 | global.CSSHINT_INVALID_ALL_COUNT++; 66 | } 67 | } 68 | }); 69 | } 70 | } 71 | ); 72 | -------------------------------------------------------------------------------- /src/rule/star-property-hack.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file star-property-hack 的检测逻辑 3 | * Checks for the star property hack (targets IE6/7) 4 | * https://github.com/CSSLint/csslint/wiki/Disallow-star-hack 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import postcss from 'postcss'; 9 | 10 | import {getLineContent, changeColorByStartAndEndIndex} from '../util'; 11 | 12 | /** 13 | * 当前文件所代表的规则名称 14 | * 15 | * @const 16 | * @type {string} 17 | */ 18 | const RULENAME = 'star-property-hack'; 19 | 20 | /** 21 | * 错误的信息 22 | * 23 | * @const 24 | * @type {string} 25 | */ 26 | const MSG = 'Disallow properties with a star prefix'; 27 | 28 | /** 29 | * 具体的检测逻辑 30 | * 31 | * @param {Object} opts 参数 32 | * @param {*} opts.ruleVal 当前规则具体配置的值 33 | * @param {string} opts.fileContent 文件内容 34 | * @param {string} opts.filePath 文件路径 35 | */ 36 | export const check = postcss.plugin(RULENAME, opts => 37 | (css, result) => { 38 | 39 | if (opts.ruleVal) { 40 | 41 | css.walkDecls(decl => { 42 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 43 | return; 44 | } 45 | 46 | const before = decl.raws.before; 47 | 48 | if (before.slice(-1) === '*') { 49 | const source = decl.source; 50 | const line = source.start.line; 51 | const lineContent = getLineContent(line, source.input.css); 52 | const col = source.start.column; 53 | result.warn(RULENAME, { 54 | node: decl, 55 | ruleName: RULENAME, 56 | line: line, 57 | col: col, 58 | message: MSG, 59 | colorMessage: '`' 60 | + changeColorByStartAndEndIndex( 61 | lineContent, col, source.end.column 62 | ) 63 | + '` ' 64 | }); 65 | 66 | global.CSSHINT_INVALID_ALL_COUNT++; 67 | } 68 | }); 69 | } 70 | } 71 | ); 72 | -------------------------------------------------------------------------------- /src/rule/text-indent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file text-indent 的检测逻辑 3 | * Checks for text indent less than -99px 4 | * https://github.com/CSSLint/csslint/wiki/Disallow-negative-text-indent 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | import {getLineContent, getPropertyValue} from '../util'; 12 | 13 | /** 14 | * 当前文件所代表的规则名称 15 | * 16 | * @const 17 | * @type {string} 18 | */ 19 | const RULENAME = 'text-indent'; 20 | 21 | /** 22 | * 错误的信息 23 | * 24 | * @const 25 | * @type {string} 26 | */ 27 | const MSG = '' 28 | + 'Negative text-indent doesn\'t work well with RTL.' 29 | + 'If you use text-indent for image replacement explicitly set direction for that item to ltr'; 30 | 31 | let textIndentDecl; 32 | let direction; 33 | 34 | /** 35 | * 具体的检测逻辑 36 | * 37 | * @param {Object} opts 参数 38 | * @param {*} opts.ruleVal 当前规则具体配置的值 39 | * @param {string} opts.fileContent 文件内容 40 | * @param {string} opts.filePath 文件路径 41 | */ 42 | export const check = postcss.plugin(RULENAME, opts => 43 | (css, result) => { 44 | if (!opts.ruleVal) { 45 | return; 46 | } 47 | 48 | css.walkRules(rule => { 49 | 50 | textIndentDecl = false; 51 | direction = 'inherit'; 52 | 53 | rule.walkDecls(decl => { 54 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 55 | return; 56 | } 57 | const prop = decl.prop; 58 | const value = getPropertyValue(decl.value); 59 | 60 | if (prop === 'text-indent' && value[0].value < -99) { 61 | textIndentDecl = decl; 62 | } 63 | else if (prop === 'direction' && value.value === 'ltr') { 64 | direction = 'ltr'; 65 | } 66 | }); 67 | 68 | if (textIndentDecl && direction !== 'ltr') { 69 | const source = textIndentDecl.source; 70 | const line = source.start.line; 71 | const lineContent = getLineContent(line, source.input.css); 72 | const col = source.start.column; 73 | result.warn(RULENAME, { 74 | node: rule, 75 | ruleName: RULENAME, 76 | line: line, 77 | col: col, 78 | message: MSG, 79 | colorMessage: '`' 80 | + lineContent.replace( 81 | textIndentDecl.prop, 82 | chalk.magenta(textIndentDecl.prop) 83 | ) 84 | + '` ' 85 | + chalk.grey(MSG) 86 | }); 87 | global.CSSHINT_INVALID_ALL_COUNT++; 88 | } 89 | }); 90 | } 91 | ); 92 | -------------------------------------------------------------------------------- /src/rule/underscore-property-hack.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file underscore-property-hack 的检测逻辑 3 | * Checks for the underscore property hack (targets IE6) 4 | * https://github.com/CSSLint/csslint/wiki/Disallow-underscore-hack 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import postcss from 'postcss'; 9 | 10 | import {getLineContent, changeColorByStartAndEndIndex} from '../util'; 11 | 12 | /** 13 | * 当前文件所代表的规则名称 14 | * 15 | * @const 16 | * @type {string} 17 | */ 18 | const RULENAME = 'underscore-property-hack'; 19 | 20 | /** 21 | * 错误的信息 22 | * 23 | * @const 24 | * @type {string} 25 | */ 26 | const MSG = 'Disallow properties with a underscore prefix'; 27 | 28 | /** 29 | * 具体的检测逻辑 30 | * 31 | * @param {Object} opts 参数 32 | * @param {*} opts.ruleVal 当前规则具体配置的值 33 | * @param {string} opts.fileContent 文件内容 34 | * @param {string} opts.filePath 文件路径 35 | */ 36 | export const check = postcss.plugin(RULENAME, opts => 37 | (css, result) => { 38 | 39 | if (opts.ruleVal) { 40 | 41 | css.walkDecls(decl => { 42 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 43 | return; 44 | } 45 | 46 | const before = decl.raws.before; 47 | 48 | if (before.slice(-1) === '_') { 49 | const source = decl.source; 50 | const line = source.start.line; 51 | const lineContent = getLineContent(line, source.input.css); 52 | const col = source.start.column; 53 | result.warn(RULENAME, { 54 | node: decl, 55 | ruleName: RULENAME, 56 | line: line, 57 | col: col, 58 | message: MSG, 59 | colorMessage: '`' 60 | + changeColorByStartAndEndIndex( 61 | lineContent, col, source.end.column 62 | ) 63 | + '` ' 64 | }); 65 | 66 | global.CSSHINT_INVALID_ALL_COUNT++; 67 | } 68 | }); 69 | } 70 | } 71 | ); 72 | -------------------------------------------------------------------------------- /src/rule/unifying-font-family-case-sensitive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file unifying-font-family-case-sensitive 的检测逻辑 3 | * 036: [强制] `font-family` 不区分大小写,但在同一个项目中,同样的 `Family Name` 大小写必须统一。 4 | * @author ielgnaw(wuji0223@gmail.com) 5 | */ 6 | 7 | import chalk from 'chalk'; 8 | import postcss from 'postcss'; 9 | 10 | import {getLineContent} from '../util'; 11 | 12 | /** 13 | * 当前文件所代表的规则名称 14 | * 15 | * @const 16 | * @type {string} 17 | */ 18 | const RULENAME = 'unifying-font-family-case-sensitive'; 19 | 20 | /** 21 | * 错误的信息 22 | * 23 | * @const 24 | * @type {string} 25 | */ 26 | const MSG = '`font-family` case insensitive, but in the same project, the same` Family Name` case must be unified.'; 27 | 28 | /** 29 | * 获取错误信息 30 | * 31 | * @param {string} curFontFamily 当前检测的这个 font-family 值 32 | * @param {string} projFontFamily 项目级别对应的这个 font-family 值 33 | * 34 | * @return {Object} 错误信息 35 | */ 36 | const getMsg = (curFontFamily, projFontFamily) => { 37 | return { 38 | msg: MSG 39 | + ' In currently project, ' 40 | + '`' 41 | + curFontFamily 42 | + '` should be `' 43 | + projFontFamily 44 | + '`.', 45 | colorMsg: MSG 46 | + ' In currently project, ' 47 | + '`' 48 | + chalk.magenta(curFontFamily) 49 | + '` should be `' 50 | + chalk.magenta(projFontFamily) 51 | + '`.' 52 | }; 53 | }; 54 | 55 | /** 56 | * 具体的检测逻辑 57 | * 58 | * @param {Object} opts 参数 59 | * @param {*} opts.ruleVal 当前规则具体配置的值 60 | * @param {string} opts.fileContent 文件内容 61 | * @param {string} opts.filePath 文件路径 62 | */ 63 | export const check = postcss.plugin(RULENAME, opts => 64 | (css, result) => { 65 | if (!opts.ruleVal) { 66 | return; 67 | } 68 | 69 | css.walkDecls(decl => { 70 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 71 | return; 72 | } 73 | 74 | const prop = decl.prop; 75 | 76 | if (prop === 'font-family') { 77 | const parts = postcss.list.space(decl.value); 78 | for (let i = 0, len = parts.length; i < len; i++) { 79 | const part = parts[i].replace(/['",]/g, ''); 80 | const partLowerCase = part.toLowerCase(); 81 | 82 | if (!global.CSSHINT_FONTFAMILY_CASE_FLAG[partLowerCase]) { 83 | global.CSSHINT_FONTFAMILY_CASE_FLAG[partLowerCase] = part; 84 | } 85 | else { 86 | if (global.CSSHINT_FONTFAMILY_CASE_FLAG[partLowerCase] !== part) { 87 | const source = decl.source; 88 | const line = source.start.line; 89 | const lineContent = getLineContent(line, source.input.css); 90 | const col = lineContent.indexOf(part) + 1; 91 | 92 | const m = getMsg(part, global.CSSHINT_FONTFAMILY_CASE_FLAG[partLowerCase]); 93 | 94 | result.warn(RULENAME, { 95 | node: decl, 96 | ruleName: RULENAME, 97 | line: line, 98 | col: col, 99 | message: m.msg, 100 | colorMessage: '`' 101 | + lineContent.replace(part, chalk.magenta(part)) 102 | + '` ' 103 | + chalk.grey(m.colorMsg) 104 | }); 105 | global.CSSHINT_INVALID_ALL_COUNT++; 106 | } 107 | } 108 | } 109 | } 110 | }); 111 | } 112 | ); 113 | -------------------------------------------------------------------------------- /src/rule/unique-headings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file unique-headings 的检测逻辑 3 | * Headings should be defined only once 4 | * https://github.com/CSSLint/csslint/wiki/Headings-should-only-be-defined-once 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | import {getLineContent} from '../util'; 12 | 13 | /** 14 | * 当前文件所代表的规则名称 15 | * 16 | * @const 17 | * @type {string} 18 | */ 19 | const RULENAME = 'unique-headings'; 20 | 21 | /** 22 | * 错误的信息 23 | * 24 | * @const 25 | * @type {string} 26 | */ 27 | const MSG = 'Headings should be defined only once'; 28 | 29 | /** 30 | * css 组合的正则匹配 31 | * 32 | * @const 33 | * @type {RegExp} 34 | */ 35 | const PATTERN_COMBINATORS = /[\s>+~]+/g; 36 | 37 | /** 38 | * 具体的检测逻辑 39 | * 40 | * @param {Object} opts 参数 41 | * @param {*} opts.ruleVal 当前规则具体配置的值 42 | * @param {string} opts.fileContent 文件内容 43 | * @param {string} opts.filePath 文件路径 44 | */ 45 | export const check = postcss.plugin(RULENAME, opts => 46 | (css, result) => { 47 | 48 | if (!opts.ruleVal) { 49 | return; 50 | } 51 | 52 | const headings = { 53 | h1: 0, 54 | h2: 0, 55 | h3: 0, 56 | h4: 0, 57 | h5: 0, 58 | h6: 0 59 | }; 60 | 61 | css.walkRules(rule => { 62 | /* jshint maxstatements: 26 */ 63 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 64 | return; 65 | } 66 | 67 | const {selector, source} = rule; 68 | const selectorGroup = selector.split(','); 69 | let line = source.start.line; 70 | let col = source.start.column; 71 | let lineContent = getLineContent(line, source.input.css); 72 | 73 | for (let i = 0, len = selectorGroup.length; i < len; i++) { 74 | const selectorInGroup = selectorGroup[i] || ''; 75 | const segments = selectorInGroup.split(PATTERN_COMBINATORS); 76 | const segmentLen = segments.length; 77 | 78 | const lastSegment = segments[segmentLen - 1]; 79 | if (!lastSegment.match(':') && headings.hasOwnProperty(lastSegment)) { 80 | headings[lastSegment]++; 81 | if (headings[lastSegment] > 1) { 82 | const newLineMatch = selectorInGroup.match(/\n/g); 83 | let extraLine = 0; 84 | if (newLineMatch) { 85 | extraLine += newLineMatch.length; 86 | line = line + extraLine; 87 | lineContent = getLineContent(line, source.input.css); 88 | col = col + lineContent.indexOf(lastSegment); 89 | } 90 | else { 91 | col = lineContent.indexOf(lastSegment) + 1; 92 | } 93 | result.warn(RULENAME, { 94 | node: rule, 95 | ruleName: RULENAME, 96 | line: line, 97 | col: col, 98 | message: MSG, 99 | colorMessage: '`' 100 | + lineContent.replace(lastSegment, chalk.magenta(lastSegment)) 101 | + '` ' 102 | + chalk.grey(MSG) 103 | }); 104 | 105 | global.CSSHINT_INVALID_ALL_COUNT++; 106 | } 107 | } 108 | } 109 | }); 110 | } 111 | ); 112 | -------------------------------------------------------------------------------- /src/rule/universal-selector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file universal-selector 的检测逻辑 3 | * Don't use universal selector because it's slow 4 | * https://github.com/CSSLint/csslint/wiki/Disallow-universal-selector 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | import {getLineContent} from '../util'; 12 | 13 | /** 14 | * 当前文件所代表的规则名称 15 | * 16 | * @const 17 | * @type {string} 18 | */ 19 | const RULENAME = 'universal-selector'; 20 | 21 | /** 22 | * css 组合的正则匹配 23 | * 24 | * @const 25 | * @type {RegExp} 26 | */ 27 | const PATTERN_COMBINATORS = /[\s>+~]+/g; 28 | 29 | /** 30 | * 错误的信息 31 | * 32 | * @const 33 | * @type {string} 34 | */ 35 | const MSG = 'Don\'t use universal selector because it\'s slow'; 36 | 37 | /** 38 | * 具体的检测逻辑 39 | * 40 | * @param {Object} opts 参数 41 | * @param {*} opts.ruleVal 当前规则具体配置的值 42 | * @param {string} opts.fileContent 文件内容 43 | * @param {string} opts.filePath 文件路径 44 | */ 45 | export const check = postcss.plugin(RULENAME, opts => 46 | (css, result) => { 47 | 48 | if (!opts.ruleVal) { 49 | return; 50 | } 51 | 52 | css.walkRules(rule => { 53 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 54 | return; 55 | } 56 | 57 | const {selector, source} = rule; 58 | const selectorGroup = selector.split(','); 59 | let line = source.start.line; 60 | let lineContent = getLineContent(line, source.input.css); 61 | 62 | for (let i = 0, len = selectorGroup.length; i < len; i++) { 63 | let selectorInGroup = selectorGroup[i] || ''; 64 | // 去掉 attr 选择器 65 | selectorInGroup = selectorInGroup.replace(/\[.+?\](?::[^\s>+~\.#\[]+)?/g, ''); 66 | 67 | const segments = selectorInGroup.split(PATTERN_COMBINATORS); 68 | const l = segments.length; 69 | if (l) { 70 | if (segments[l - 1] === '*') { 71 | if (selectorInGroup.slice(0, 1) === '\n') { 72 | line = line + 1; 73 | lineContent = getLineContent(line, source.input.css); 74 | } 75 | const col = lineContent.indexOf(segments[l - 1]) + 1; 76 | result.warn(RULENAME, { 77 | node: rule, 78 | ruleName: RULENAME, 79 | line: line, 80 | col: col, 81 | message: MSG, 82 | colorMessage: '`' 83 | + lineContent.replace(segments[l - 1], chalk.magenta(segments[l - 1])) 84 | + '` ' 85 | + chalk.grey(MSG) 86 | }); 87 | 88 | global.CSSHINT_INVALID_ALL_COUNT++; 89 | } 90 | } 91 | } 92 | }); 93 | } 94 | ); 95 | -------------------------------------------------------------------------------- /src/rule/unqualified-attributes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file unqualified-attributes 的检测逻辑 3 | * Unqualified attribute selectors are known to be slow 4 | * https://github.com/CSSLint/csslint/wiki/Disallow-unqualified-attribute-selectors 5 | * @author ielgnaw(wuji0223@gmail.com) 6 | */ 7 | 8 | import chalk from 'chalk'; 9 | import postcss from 'postcss'; 10 | 11 | import {getLineContent} from '../util'; 12 | 13 | /** 14 | * 当前文件所代表的规则名称 15 | * 16 | * @const 17 | * @type {string} 18 | */ 19 | const RULENAME = 'unqualified-attributes'; 20 | 21 | /** 22 | * css 组合的正则匹配 23 | * 24 | * @const 25 | * @type {RegExp} 26 | */ 27 | const PATTERN_COMBINATORS = /[\s>+~]+/g; 28 | 29 | /** 30 | * 错误的信息 31 | * 32 | * @const 33 | * @type {string} 34 | */ 35 | const MSG = 'Unqualified attribute selectors are known to be slow'; 36 | 37 | /** 38 | * 具体的检测逻辑 39 | * 40 | * @param {Object} opts 参数 41 | * @param {*} opts.ruleVal 当前规则具体配置的值 42 | * @param {string} opts.fileContent 文件内容 43 | * @param {string} opts.filePath 文件路径 44 | */ 45 | export const check = postcss.plugin(RULENAME, opts => 46 | (css, result) => { 47 | 48 | if (!opts.ruleVal) { 49 | return; 50 | } 51 | 52 | css.walkRules(rule => { 53 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 54 | return; 55 | } 56 | 57 | const {selector, source} = rule; 58 | const selectorGroup = selector.split(','); 59 | let line = source.start.line; 60 | let lineContent = getLineContent(line, source.input.css); 61 | 62 | for (let i = 0, len = selectorGroup.length; i < len; i++) { 63 | const selectorInGroup = selectorGroup[i] || ''; 64 | const segments = selectorInGroup.split(PATTERN_COMBINATORS); 65 | const l = segments.length; 66 | if (l) { 67 | const last = segments[l - 1]; 68 | if (last.match(/\[.+?\](?::[^\s>+~\.#\[]+)?/g)) { 69 | if (selectorInGroup.slice(0, 1) === '\n') { 70 | line = line + 1; 71 | lineContent = getLineContent(line, source.input.css); 72 | } 73 | const col = lineContent.indexOf(segments[l - 1]) + 1; 74 | result.warn(RULENAME, { 75 | node: rule, 76 | ruleName: RULENAME, 77 | line: line, 78 | col: col, 79 | message: MSG, 80 | colorMessage: '`' 81 | + lineContent.replace(segments[l - 1], chalk.magenta(segments[l - 1])) 82 | + '` ' 83 | + chalk.grey(MSG) 84 | }); 85 | 86 | global.CSSHINT_INVALID_ALL_COUNT++; 87 | } 88 | } 89 | } 90 | }); 91 | } 92 | ); 93 | -------------------------------------------------------------------------------- /src/rule/zero-unit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file zero-unit 的检测逻辑 3 | * 028: [强制] 长度为 `0` 时须省略单位。 (也只有长度单位可省) 4 | * @author ielgnaw(wuji0223@gmail.com) 5 | */ 6 | 7 | import chalk from 'chalk'; 8 | import postcss from 'postcss'; 9 | 10 | import {getLineContent} from '../util'; 11 | 12 | 'use strict'; 13 | 14 | /** 15 | * 规则名称 16 | * 17 | * @const 18 | * @type {string} 19 | */ 20 | const RULENAME = 'zero-unit'; 21 | 22 | /** 23 | * css 长度单位集合 24 | * https://developer.mozilla.org/en-US/docs/Web/CSS/length 25 | * 26 | * @const 27 | * @type {Array} 28 | */ 29 | const LENGTH_UNITS = [ 30 | // Relative length units 31 | 'em', 'ex', 'ch', 'rem', // Font-relative lengths 32 | 'vh', 'vw', 'vmin', 'vmax', // Viewport-percentage lengths 33 | // Absolute length units 34 | 'px', 'mm', 'cm', 'in', 'pt', 'pc' 35 | ]; 36 | 37 | /** 38 | * 数字正则 39 | * 40 | * @const 41 | * @type {RegExp} 42 | */ 43 | const PATTERN_NUMERIC = /\d+[\.\d]*/; 44 | 45 | /** 46 | * 错误信息 47 | * 48 | * @const 49 | * @type {string} 50 | */ 51 | const MSG = 'Values of 0 shouldn\'t have units specified'; 52 | 53 | /** 54 | * 行号的缓存,防止同一行多次报错 55 | * 56 | * @type {number} 57 | */ 58 | let lineCache = 0; 59 | 60 | /** 61 | * 具体的检测逻辑 62 | * 63 | * @param {Object} opts 参数 64 | * @param {*} opts.ruleVal 当前规则具体配置的值 65 | * @param {string} opts.fileContent 文件内容 66 | * @param {string} opts.filePath 文件路径 67 | */ 68 | export const check = postcss.plugin(RULENAME, opts => 69 | (css, result) => { 70 | if (!opts.ruleVal) { 71 | return; 72 | } 73 | 74 | if (global.CSSHINT_INVALID_ALL_COUNT >= opts.maxError) { 75 | return; 76 | } 77 | 78 | lineCache = 0; 79 | 80 | css.walkDecls(decl => { 81 | const parts = postcss.list.space(decl.value); 82 | for (let i = 0, len = parts.length; i < len; i++) { 83 | const part = parts[i]; 84 | const numericVal = parseFloat(part); 85 | 86 | if (numericVal === 0) { 87 | const unit = part.replace(PATTERN_NUMERIC, ''); 88 | const source = decl.source; 89 | const line = source.start.line; 90 | 91 | if (LENGTH_UNITS.indexOf(unit) > -1 && lineCache !== line) { 92 | lineCache = line; 93 | const lineContent = getLineContent(line, source.input.css); 94 | result.warn(RULENAME, { 95 | node: decl, 96 | ruleName: RULENAME, 97 | line: line, 98 | col: source.start.column + decl.prop.length + decl.raws.between.length, 99 | message: MSG, 100 | colorMessage: '`' 101 | + lineContent.replace( 102 | decl.value, 103 | chalk.magenta(decl.value) 104 | ) 105 | + '` ' 106 | + chalk.grey(MSG) 107 | }); 108 | global.CSSHINT_INVALID_ALL_COUNT++; 109 | } 110 | } 111 | } 112 | }); 113 | } 114 | ); 115 | -------------------------------------------------------------------------------- /test/fixture/.csshintignore: -------------------------------------------------------------------------------- 1 | csshintignore.css 2 | -------------------------------------------------------------------------------- /test/fixture/.csshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "max-error": 1000, 3 | "no-bom": true, 4 | "require-before-space": ["{"], 5 | "require-after-space": [":", ","], 6 | "max-length": 120, 7 | "disallow-important": true, 8 | "require-around-space": [ "+", "~", ">"], 9 | "max-selector-nesting-level": 3, 10 | "require-newline": ["selector", "property", "media-query-condition"] 11 | } 12 | -------------------------------------------------------------------------------- /test/fixture/.csshintrc.yml: -------------------------------------------------------------------------------- 1 | max-error: 1000 2 | no-bom: true 3 | require-before-space: ['{'] 4 | require-after-space: 5 | - ':' 6 | - ',' 7 | max-length: 120 8 | disallow-important: true 9 | require-around-space: 10 | - '+' 11 | - '~' 12 | - '>' 13 | max-selector-nesting-level: 3 14 | require-newline: 15 | - 'selector' 16 | - 'property' 17 | - 'media-query-condition' 18 | -------------------------------------------------------------------------------- /test/fixture/adjoining-classes.css: -------------------------------------------------------------------------------- 1 | .cc .foo.bar { 2 | color: #f00; 3 | } 4 | 5 | .foo.bar { 6 | color: #f00; 7 | } 8 | -------------------------------------------------------------------------------- /test/fixture/always-semicolon.css: -------------------------------------------------------------------------------- 1 | div { 2 | margin: 0 3 | } 4 | 5 | span { 6 | padding: 0 7 | } 8 | 9 | p { 10 | max-width: 500px !important 11 | } 12 | .function {padding-bottom:20px;border-bottom:1px solid #e3e3e3} 13 | 14 | 15 | .cc { 16 | display: inline-block; 17 | *display: inline 18 | } 19 | 20 | -------------------------------------------------------------------------------- /test/fixture/always-semicolon2.css: -------------------------------------------------------------------------------- 1 | div { 2 | margin: 0 3 | } 4 | 5 | span { 6 | padding: 0 7 | } 8 | 9 | p { 10 | max-width: 500px !important 11 | } 12 | .function {padding-bottom:20px;border-bottom:1px solid #e3e3e3} 13 | 14 | 15 | .cc { 16 | display: inline-block; 17 | *display: inline 18 | } 19 | 20 | -------------------------------------------------------------------------------- /test/fixture/block-indent-new1.css: -------------------------------------------------------------------------------- 1 | /* csshint block-indent: [" ", 3]*/ 2 | 3 | body { 4 | margin: 0; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixture/block-indent-new2.css: -------------------------------------------------------------------------------- 1 | .closure.modal > .content > .image { 2 | display: block; 3 | width: ""; 4 | -webkit-box-flex: 0; 5 | -webkit-flex: 0 1 auto; 6 | -ms-flex: 0 1 auto; 7 | flex: 0 1 auto; 8 | -webkit-align-self: top; 9 | align-self: top; 10 | -ms-flex-item-align: top; 11 | } 12 | -------------------------------------------------------------------------------- /test/fixture/block-indent.css: -------------------------------------------------------------------------------- 1 | /* csshint-disable no-bom */ /* csshint block-indent: [" ", 0]*/ 2 | body{ 3 | height: 10; 4 | width: 2px; 5 | aa:12px;width: 12px; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixture/block-indent1.css: -------------------------------------------------------------------------------- 1 | @keyframes errorbg { 2 | 0%, 3 | 25% { 4 | background: #f5cfc4; 5 | _opacity: .18; 6 | } 7 | 100% { 8 | background: #f5cfc4; 9 | opacity: .18; 10 | } 11 | } 12 | 13 | body ,b,a { 14 | color: #fff; 15 | } 16 | 17 | @media handheld and (min-width:360px), screen and (min-width:480px) { 18 | body {_color: #fff; 19 | } 20 | 21 | } 22 | 23 | @media screen and (orientation:portrait) { 24 | @media screen and (min-device-width: 360px) { 25 | #footerLogo { 26 | *margin-top: 5px; 27 | } 28 | } 29 | @media screen and (min-device-width: 500px) { 30 | #footerLogo { 31 | margin-top: 15px; 32 | } 33 | } 34 | } 35 | @media screen and (orientation:landscape) { 36 | #container { 37 | width: 320px; 38 | margin: 0 auto; 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /test/fixture/block-indent2.css: -------------------------------------------------------------------------------- 1 | a { 2 | color: #fff; 3 | } 4 | @media handheld and (min-width:360px), screen and (min-width:480px) { 5 | body { 6 | font-size:large; 7 | } 8 | } 9 | 10 | @keyframes errorbg { 11 | 0%, 12 | 25% { 13 | background: #f5cfc4; 14 | opacity: .18; 15 | } 16 | 100% { 17 | background: #f5cfc4; 18 | opacity: .18; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/fixture/block-indent3.css: -------------------------------------------------------------------------------- 1 | @media all and (min-width: 200px) and (max-width: 500px) { 2 | body { 3 | background-color: #F5F6F8; 4 | } 5 | .main { 6 | width: 100%; 7 | } 8 | .main .top { 9 | width: 95%; 10 | font-size: .9rem; 11 | color: #fff; 12 | background-color: #00CCD3; 13 | line-height: 32px; 14 | height: 32px; 15 | padding-left: 5%; 16 | position: relative; 17 | } 18 | /* .main .topic-category-wrap, 19 | .main .main-article { 20 | display: none; 21 | } */ 22 | .main .topic-category-wrap { 23 | display: block; 24 | } 25 | .main .main-article { 26 | position: relative; 27 | left: 0; 28 | top: 0; 29 | width: 100%; 30 | display: none; 31 | } 32 | .main .main-article .main-content { 33 | width: 100%; 34 | } 35 | .main-article .main-content .article { 36 | position: relative; 37 | width: 78%; 38 | height: 10rem; 39 | background-color: #fff; 40 | border: 1px solid #DCDCDC; 41 | margin: 0 auto; 42 | margin-top: .5rem; 43 | padding: 6%; 44 | } 45 | .main-content .article .article-title { 46 | display: -webkit-box; 47 | -webkit-box-orient: vertical; 48 | overflow: hidden; 49 | text-overflow: ellipsis; 50 | -webkit-line-clamp: 2; 51 | font-size: .9rem; 52 | color: #333; 53 | } 54 | .main-content .article .article-content { 55 | margin-top: .5rem; 56 | width: 100%; 57 | height: 7rem; 58 | font-size: .7rem; 59 | color: #333; 60 | word-break: break-word; 61 | 62 | } 63 | .main-content .article .article-expert { 64 | position: absolute; 65 | left: 0; 66 | width: 94%; 67 | height: 1rem; 68 | border-top: 1px solid #DCDCDC; 69 | padding-left: 6%; 70 | 71 | } 72 | .top img { 73 | width: 4%; 74 | margin-left: 30%; 75 | } 76 | .top .share { 77 | color: #fff; 78 | } 79 | 80 | .top .bdsharebuttonbox { 81 | display: none; 82 | position: absolute; 83 | left: 70%; 84 | color: #fff; 85 | } 86 | 87 | .tab { 88 | width: 100%; 89 | font-size: .7rem; 90 | overflow: hidden; 91 | border-bottom: 1px solid #00CCD3; 92 | color: #333; 93 | } 94 | 95 | .tab .tab-inner { 96 | position: relative; 97 | display: -webkit-box; 98 | display: box; 99 | -webkit-box-pack: start; 100 | box-pack: start; 101 | -webkit-box-orient: horizontal; 102 | box-orient: horizontal; 103 | line-height: 44px; 104 | } 105 | .tab .tab-inner .selected { 106 | position: relative; 107 | border-bottom: 2px solid #00CCD3; 108 | } 109 | .tab .tab-inner a { 110 | display: block; 111 | -webkit-box-flex: 1; 112 | box-flex: 1; 113 | text-align: center; 114 | color: #333; 115 | margin-left: 10px; 116 | } 117 | .main .category, 118 | .main .topic { 119 | width: 100%; 120 | padding-top: 5%; 121 | } 122 | .main .category .category-topic-inner, 123 | .main .topic .category-topic-inner { 124 | display: -webkit-flex; 125 | display: flex; 126 | -webkit-flex-wrap: wrap; 127 | justify-content: center; 128 | width: 100%; 129 | 130 | } 131 | .main .category .category-topic-content, 132 | .main .topic .category-topic-content { 133 | text-align: center; 134 | color: #333; 135 | border: 1px solid #b0b1b1; 136 | border-shadow: 3px; 137 | font-size: .7rem; 138 | height: 1rem; 139 | text-align: center; 140 | line-height: 1rem; 141 | margin-left: 8px; 142 | margin-bottom: 8px; 143 | width: 28%; 144 | } 145 | .main .category .category-topic-no-content, 146 | .main .topic .category-topic--no-content { 147 | border: 0; 148 | } 149 | 150 | 151 | .box { 152 | display: -webkit-box; 153 | webkit-box-orient: vertical; 154 | border: 1px solid red; 155 | overflow: hidden; 156 | text-overflow: ellipsis; 157 | -webkit-line-clamp: 2; 158 | -webkit-box-orient: vertical; 159 | } 160 | 161 | .footer { 162 | width: 94%; 163 | height: 1rem; 164 | padding-left: 6%; 165 | margin-top: 1rem; 166 | margin-bottom: 1rem; 167 | font-size: .7rem; 168 | font-weight: 700; 169 | color: #a0a0a0; 170 | display: none; 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /test/fixture/block-indent4.css: -------------------------------------------------------------------------------- 1 | body{ 2 | height: 10; 3 | width: 2px; 4 | aa:12px; 5 | width: 12px; 6 | } 7 | .CWSIR_entry_icon{display:inline-block;width:33px;height:33px;position:relative;background:url("http://cwsir.sinaapp.com/CWSirExtensions/images/icon.png") no-repeat -120px 0;z-index:10;} 8 | 9 | @media handheld and (min-width:360px),screen and (min-width:480px) { 10 | body { 11 | font-size:large; 12 | } 13 | } 14 | 15 | 16 | @keyframes errorbg { 17 | 0%, 18 | 25% { 19 | background: #f5cfc4; 20 | opacity: .18; 21 | } 22 | 100% { 23 | background: #f5cfc4; 24 | opacity: .18; 25 | } 26 | } 27 | 28 | body ,b,a { 29 | color: #fff; 30 | } 31 | 32 | @media handheld and (min-width:360px), screen and (min-width:480px) { 33 | body { 34 | color: #fff; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /test/fixture/block-indent5.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixture/block-indent6.css: -------------------------------------------------------------------------------- 1 | 2 | .branch-box { 3 | border-radius: 10px; 4 | background: #f5f5f5 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAYAAABy6+R8AAAAb0lEQVR42p2RMQ7AIAhFWXsG1h7Gtffo6urqyJGpwyetomnwJ38BH/wgfZSaS7PABbVOqkqm3Fzx6IQTatlDb5PJi21YBy1iuNgjJIgzFXqyCcXj1Z1DXNGTiw01aP252DCCgH7Fk41h8KaAGMDxADnaOPucd/m3AAAAAElFTkSuQmCC) no-repeat 7px 6px;; 5 | background-position-x: 170px; 6 | background-position-y: 5px; 7 | padding-left: 10px; 8 | padding-right: 40px; 9 | } 10 | -------------------------------------------------------------------------------- /test/fixture/block-indent7.css: -------------------------------------------------------------------------------- 1 | /** 2 | * 注意 div 的 } 符号后有空格 3 | */ 4 | 5 | div { 6 | margin-top: -4px; 7 | } 8 | a { 9 | border: 0 solid #fff; 10 | } -------------------------------------------------------------------------------- /test/fixture/block-indent8.css: -------------------------------------------------------------------------------- 1 | /* csshint block-indent: [" ", 4] */ 2 | 3 | body { 4 | margin: 0px; 5 | padding: 0 6 | } -------------------------------------------------------------------------------- /test/fixture/box-model.css: -------------------------------------------------------------------------------- 1 | /* width and border with box-sizing */ 2 | .mybox { 3 | box-sizing: border-box; 4 | border: 1px solid black; 5 | width: 100px; 6 | } 7 | 8 | /* width and border-top */ 9 | .mybox { 10 | border-top: 1px solid black; 11 | width: 100px; 12 | } 13 | 14 | /* height and border-top of none */ 15 | .mybox { 16 | border-top: none; 17 | height: 100px; 18 | } 19 | 20 | /* width and border */ 21 | .mybox { 22 | border: 1px solid black; 23 | width: 100px; 24 | } 25 | -------------------------------------------------------------------------------- /test/fixture/box-sizing.css: -------------------------------------------------------------------------------- 1 | .box { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixture/bulletproof-font-face.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'MyFontFamily'; 3 | src: url('myfont-webfont.eot?#iefix') format('embedded-opentype'), 4 | url('myfont-webfont.woff') format('woff'), 5 | url('myfont-webfont.ttf') format('truetype'), 6 | url('myfont-webfont.svg#svgFontName') format('svg'); 7 | } 8 | 9 | @font-face { 10 | font-family: 'HarlowSolid'; 11 | src: url('harlowsi-webfont.eot?') format('eot'), 12 | url('harlowsi-webfont.woff') format('woff'), 13 | url('harlowsi-webfont.ttf') format('truetype'), 14 | url('harlowsi-webfont.svg#harlowsi-webfont') format('svg'); 15 | } 16 | -------------------------------------------------------------------------------- /test/fixture/cr.css: -------------------------------------------------------------------------------- 1 | /*.modal-content { 2 | position: relative; 3 | background-color: #fff; 4 | border: 1px solid #999; 5 | border: 1px solid rgba(0, 0, 0, .2); 6 | outline: 0; 7 | -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5); 8 | box-shadow: 0 3px 9px rgba(0, 0, 0, .5); 9 | -webkit-background-clip: padding-box; 10 | background-clip: padding-box; 11 | }*/ 12 | 13 | /*.radius { 14 | -webkit-border-radius: 5px; 15 | border-radius: 5px; 16 | -webkit-animation: spin 2s infinite linear; 17 | animation: spin 2s infinite linear; 18 | }*/ 19 | 20 | .modal-content { 21 | -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5); 22 | box-shadow: 0 3px 9px rgba(0, 0, 0, .5); 23 | } 24 | 25 | @media (min-width: 768px) { 26 | .modal-content { 27 | -webkit-box-shadow: 0 5px 15px rgba(100, 0, 0, .5); 28 | box-shadow: 0 5px 15px rgba(100, 0, 0, .5); 29 | } 30 | @media (min-width: 168px) { 31 | .modal-content { 32 | -webkit-box-shadow: 0 8px 19px rgba(0, 200, 0, .5); 33 | box-shadow: 0 8px 19px rgba(0, 200, 0, .5); 34 | } 35 | } 36 | } 37 | 38 | @-webkit-keyframes link_float { 39 | from { 40 | opacity: 0; 41 | -webkit-transform: scale(0); 42 | transform: scale(0); 43 | } 44 | } 45 | @-moz-keyframes link_float { 46 | from { 47 | -moz-transform: scale(0); 48 | transform: scale(0); 49 | opacity: 0; 50 | } 51 | } -------------------------------------------------------------------------------- /test/fixture/cr1.css: -------------------------------------------------------------------------------- 1 | /*body{} 2 | .aaa { 3 | line-height: .7; 4 | } 5 | */ 6 | .bbb { 7 | font-family: "Microsoft Yahei", Verdana, Simsun, "Segoe UI Web Light", "Segoe UI Light", 8 | "Segoe UI Web Regular", "Segoe UI", "Segoe UI Symbol", "Helvetica Neue", Arial; 9 | } 10 | -------------------------------------------------------------------------------- /test/fixture/csshintignore.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixture/disallow-expression.css: -------------------------------------------------------------------------------- 1 | input { 2 | width: expression(onmouseover=this.style.backgroundColor="#F5F5F5"; 3 | onmouseout=this.style.backgroundColor="#FFFFFF"); 4 | } 5 | 6 | 7 | input { 8 | width: expression(this.offsetWidth > 750 ? '12px' : '20px'); 9 | } 10 | -------------------------------------------------------------------------------- /test/fixture/disallow-important.css: -------------------------------------------------------------------------------- 1 | body { 2 | color:red !important;height: 100px !important; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixture/disallow-named-color.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: red ; 3 | color: #f00; 4 | width: 100px; 5 | border: 1px solid black; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixture/disallow-overqualified-elements.css: -------------------------------------------------------------------------------- 1 | 2 | /*.a.a { 3 | color: red; 4 | }*/ 5 | 6 | /*.c .d { 7 | width: 20px; 8 | }*/ 9 | 10 | .aa { 11 | color: red; 12 | } 13 | 14 | p.bb .testp { 15 | color: green; 16 | } 17 | 18 | a:hover { 19 | color: red; 20 | } 21 | 22 | #cc { 23 | color: #fff; 24 | } 25 | 26 | div#cc { 27 | color: #000; 28 | } 29 | 30 | a.test:hover { 31 | color: #ccc; 32 | } 33 | 34 | .aad, a.ddd { 35 | color: red; 36 | } 37 | 38 | 39 | 40 | .post .cc, .page cc.dd, cc.comment cc.ee .gg{ 41 | line-height: 1.5; 42 | } 43 | 44 | a.aa { 45 | color: red; 46 | } 47 | 48 | .aad, a { 49 | color: red; 50 | } 51 | 52 | .pagesss .cc, .page .dd{ 53 | line-height: 1.5; 54 | } 55 | 56 | .aaa, hh.bbb > ll.oo, .ccc, .ddd{ 57 | color: red; 58 | } 59 | 60 | /*.CWSIR_page_tab_radious .CWSIR_page_t_itm, .CWSIR_page_tab_radious .CWSIR_page_t_opt, .CWSIR_page_tab_radious .CWSIR_page_W_vline, .aaa .bbb .ccc { 61 | float: left; 62 | display: inline; 63 | margin: 0 8px 0 0; 64 | line-height: 22px; 65 | text-align: center; 66 | }*/ 67 | 68 | /*#ccc .fff .qqq, #ccc .fff .qqq a, #ccc .fff a.cwsirrss_entry-source-title, #ccc .fff a.cwsirrss_entry-post-author-name { 69 | color: #15c; 70 | }*/ 71 | 72 | 73 | 74 | /*li.selected > a:hover > a:focus { 75 | color: #ccc; 76 | }*/ 77 | 78 | 79 | /*li.selected > a:hover {*/ 80 | /*color: red;*/ 81 | /*}*/ 82 | -------------------------------------------------------------------------------- /test/fixture/disallow-overqualified-elements1.css: -------------------------------------------------------------------------------- 1 | @keyframes btnLoader { 2 | 87.5% { 3 | box-shadow: 0 -3em 0 0 #fff, 4 | 2em -2em 0 -0.5em #fff, 5 | 3em 0 0 -0.5em #fff, 6 | 2em 2em 0 -0.5em #fff, 7 | 0 3em 0 -0.5em #fff, 8 | -2em 2em 0 0 #fff, 9 | -3em 0 0 0 #fff, 10 | -2em -2em 0 .2em #fff; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/fixture/disallow-quotes-in-url.css: -------------------------------------------------------------------------------- 1 | span { 2 | background:#fff url("http://cwsir.sinaapp.com/banner.jpg") no-repeat center 0; 3 | background:#000 url('http://baidu.com/1.jpg') no-repeat url("http://asdasd.com/sda.png") 0; 4 | /*background:#aaa url(http://qq.com/2.jpg) no-repeat center 0;*/ 5 | } 6 | -------------------------------------------------------------------------------- /test/fixture/display-property-grouping.css: -------------------------------------------------------------------------------- 1 | /* inline with height */ 2 | .mybox { 3 | display: inline; 4 | height: 25px; 5 | } 6 | 7 | /* inline-block with float */ 8 | .mybox { 9 | display: inline-block; 10 | float: left; 11 | } 12 | 13 | /* table-cell and margin */ 14 | .mybox { 15 | display: table-cell; 16 | margin: 10px; 17 | } 18 | -------------------------------------------------------------------------------- /test/fixture/duplicate-background-images.css: -------------------------------------------------------------------------------- 1 | /* multiple instances of the same URL */ 2 | .heart-icon { 3 | background: url(sprite.png) -16px 0 no-repeat; 4 | } 5 | 6 | .task-icon { 7 | background: url(sprite.png) -32px 0 no-repeat; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixture/duplicate-properties.css: -------------------------------------------------------------------------------- 1 | /* properties with the same value */ 2 | .mybox { 3 | border: 1px solid black; 4 | border: 1px solid black; 5 | } 6 | 7 | /* properties separated by another property */ 8 | .mybox { 9 | border: 1px solid black; 10 | color: green; 11 | border: 1px solid red; 12 | } 13 | 14 | 15 | /* one after another with different values */ 16 | .mybox { 17 | border: 1px solid black; 18 | border: 1px solid red; 19 | } 20 | -------------------------------------------------------------------------------- /test/fixture/empty-rules.css: -------------------------------------------------------------------------------- 1 | .mybox { } 2 | 3 | .mybox { 4 | 5 | } 6 | 7 | .mybox { 8 | /* a comment */ 9 | } 10 | @media handheld and (min-width:360px), screen and (min-width:480px) { 11 | body { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/fixture/error-char.css: -------------------------------------------------------------------------------- 1 | .wm-location-show-middle span { 2 | display: inline-block; 3 | margin-left: 4px; 4 | width: 11px; 5 | height: 11px; 6 | background:url(//m.baidu.com/static/search/appAla/weizhan/icon_how.png) no-repeat; 7 | background-size: 11px 11px; 8 | vertical-align: -1px; 9 | } 10 | -------------------------------------------------------------------------------- /test/fixture/fallback-colors.css: -------------------------------------------------------------------------------- 1 | /* missing fallback color */ 2 | .mybox { 3 | color: rgba(100, 200, 100, 0.5); 4 | } 5 | 6 | /* missing fallback color */ 7 | .mybox { 8 | background-color: green; 9 | background-color: hsla(100, 50%, 100%, 0.5); 10 | } 11 | 12 | /* missing fallback color */ 13 | .mybox { 14 | background: hsla(100, 50%, 100%, 0.5) url(foo.png); 15 | } 16 | 17 | /* fallback color should be before */ 18 | .mybox { 19 | background-color: hsl(100, 50%, 100%); 20 | background-color: green; 21 | } 22 | 23 | /* fallback color before newer format */ 24 | .mybox { 25 | color: red; 26 | color: rgba(255, 0, 0, 0.5); 27 | } 28 | -------------------------------------------------------------------------------- /test/fixture/floats.css: -------------------------------------------------------------------------------- 1 | .user-info-list { 2 | height: 90px; 3 | float: left; 4 | } 5 | 6 | .aa { 7 | height: 90px; 8 | float: left; 9 | } 10 | 11 | .bb { 12 | height: 90px; 13 | float: left; 14 | } 15 | 16 | .cc { 17 | height: 90px; 18 | float: left; 19 | } 20 | 21 | .dd { 22 | height: 90px; 23 | float: left; 24 | } 25 | 26 | .ee { 27 | height: 90px; 28 | float: left; 29 | } 30 | 31 | .ff { 32 | height: 90px; 33 | float: left; 34 | } 35 | 36 | .gg { 37 | height: 90px; 38 | float: left; 39 | } 40 | 41 | .hh { 42 | height: 90px; 43 | float: left; 44 | } 45 | 46 | .ii { 47 | height: 90px; 48 | float: left; 49 | } 50 | 51 | .jj { 52 | height: 90px; 53 | float: left; 54 | } 55 | 56 | .kk { 57 | height: 90px; 58 | float: left; 59 | } 60 | -------------------------------------------------------------------------------- /test/fixture/font-face.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'MyFontFamily'; 3 | src: url('myfont-webfont.eot?#iefix') format('embedded-opentype'), 4 | url('myfont-webfont.woff') format('woff'), 5 | url('myfont-webfont.ttf') format('truetype'), 6 | url('myfont-webfont.svg#svgFontName') format('svg'); 7 | } 8 | 9 | @font-face { 10 | font-family: 'HarlowSolid'; 11 | src: url('harlowsi-webfont.eot?') format('eot'), 12 | url('harlowsi-webfont.woff') format('woff'), 13 | url('harlowsi-webfont.ttf') format('truetype'), 14 | url('harlowsi-webfont.svg#harlowsi-webfont') format('svg'); 15 | } 16 | 17 | @font-face { 18 | font-family: 'Test'; 19 | src: url('harlowsi-webfont.eot?') format('eot'), 20 | url('harlowsi-webfont.woff') format('woff'), 21 | url('harlowsi-webfont.ttf') format('truetype'), 22 | url('harlowsi-webfont.svg#harlowsi-webfont') format('svg'); 23 | } 24 | 25 | @font-face { 26 | font-family: 'HarlowSolid'; 27 | src: url('harlowsi-webfont.eot?') format('eot'), 28 | url('harlowsi-webfont.woff') format('woff'), 29 | url('harlowsi-webfont.ttf') format('truetype'), 30 | url('harlowsi-webfont.svg#harlowsi-webfont') format('svg'); 31 | } 32 | 33 | @font-face { 34 | font-family: 'Test'; 35 | src: url('harlowsi-webfont.eot?') format('eot'), 36 | url('harlowsi-webfont.woff') format('woff'), 37 | url('harlowsi-webfont.ttf') format('truetype'), 38 | url('harlowsi-webfont.svg#harlowsi-webfont') format('svg'); 39 | } 40 | 41 | 42 | @font-face { 43 | font-family: 'HarlowSolid'; 44 | src: url('harlowsi-webfont.eot?') format('eot'), 45 | url('harlowsi-webfont.woff') format('woff'), 46 | url('harlowsi-webfont.ttf') format('truetype'), 47 | url('harlowsi-webfont.svg#harlowsi-webfont') format('svg'); 48 | } 49 | 50 | -------------------------------------------------------------------------------- /test/fixture/font-sizes.css: -------------------------------------------------------------------------------- 1 | .user-info-list { 2 | height: 90px; 3 | font-size: 13px; 4 | } 5 | 6 | .aa { 7 | height: 90px; 8 | font-size: 13px; 9 | } 10 | 11 | .bb { 12 | height: 90px; 13 | font-size: 13px; 14 | } 15 | 16 | .cc { 17 | height: 90px; 18 | font-size: 13px; 19 | } 20 | 21 | .dd { 22 | height: 90px; 23 | font-size: 13px; 24 | } 25 | 26 | .ee { 27 | height: 90px; 28 | font-size: 13px; 29 | } 30 | 31 | .ff { 32 | height: 90px; 33 | font-size: 13px; 34 | } 35 | 36 | .gg { 37 | height: 90px; 38 | font-size: 13px; 39 | } 40 | 41 | .hh { 42 | height: 90px; 43 | font-size: 13px; 44 | } 45 | 46 | .ii { 47 | height: 90px; 48 | font-size: 13px; 49 | } 50 | 51 | .jj { 52 | height: 90px; 53 | font-size: 13px; 54 | } 55 | 56 | .kk { 57 | height: 90px; 58 | font-size: 13px; 59 | } 60 | -------------------------------------------------------------------------------- /test/fixture/gradients.css: -------------------------------------------------------------------------------- 1 | /* Missing -moz, -ms, and -o */ 2 | .mybox { 3 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #1e5799), color-stop(100%, #7db9e8)); 4 | background: -webkit-linear-gradient(top, #1e5799 0%, #7db9e8 100%); 5 | } 6 | 7 | /* Missing old and new -webkit */ 8 | .mybox { 9 | background: -moz-linear-gradient(top, #1e5799 0%, #7db9e8 100%); 10 | background: -o-linear-gradient(top, #1e5799 0%, #7db9e8 100%); 11 | background: -ms-linear-gradient(top, #1e5799 0%, #7db9e8 100%); 12 | } 13 | -------------------------------------------------------------------------------- /test/fixture/has-bom.css: -------------------------------------------------------------------------------- 1 | a { 2 | color: #f00; 3 | } -------------------------------------------------------------------------------- /test/fixture/hex-color.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: rgb(255, 255, 255); 3 | color: rgb(20%, 20%, 20%); 4 | color: rgba(255, 255, 255, 1); 5 | color: hsla(120, 65%, 75%, 1); 6 | color: #fff; 7 | border: 0px solid rgb(255, 255, 255); 8 | /*border: 0px solid hsl(120, 65%, 75%);*/ 9 | /*color: rgb(11%, 11%, 11%) rgb(55%, 55%, 55%);*/ 10 | } 11 | -------------------------------------------------------------------------------- /test/fixture/horizontal-vertical-position.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-position: center; /* 50% 0% */ 3 | } 4 | -------------------------------------------------------------------------------- /test/fixture/ids.css: -------------------------------------------------------------------------------- 1 | #mybox { 2 | display: block; 3 | } 4 | 5 | .mybox #go { 6 | color: red; 7 | } 8 | 9 | .mybox #go a{ 10 | color: red; 11 | } 12 | 13 | .aaa, 14 | .cc #go a b { 15 | color: red; 16 | } 17 | -------------------------------------------------------------------------------- /test/fixture/import.css: -------------------------------------------------------------------------------- 1 | @import url(more.css); 2 | @import url(andmore.css); 3 | 4 | a { 5 | color: black; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixture/index.css: -------------------------------------------------------------------------------- 1 | body{background:#f4f4f4;color:#3a3a3a;} 2 | .fl{float:left;} 3 | .fr{float:right;} 4 | /*两种清浮动方法*/ 5 | .clearfix:after{content: ".";display: block;height: 0;clear: both;visibility: hidden;} 6 | .clearfix {display: inline-block;} 7 | /* Hides from IE-mac */ 8 | * html .clearfix {height: 1%;} 9 | .clearfix {display: block;} 10 | /* End hide from IE-mac */ 11 | .clear{clear:both;height:0;font:0/0 Arial;visibility:hidden;} 12 | /*两种清浮动方法*/ 13 | 14 | .header_wrap{background:#000;height:38px;} 15 | .header_wrap .header{width:960px;margin:0 auto;height:38px;} 16 | .header .logo{float:left;margin-top:2px;width:103px;height:38px;background:url(http://cwsir.sinaapp.com/CWSirExtensions/images/logo.png) no-repeat center 0;} 17 | .header .nickname{float:right;color:#fff;line-height:38px;} 18 | div.banner{height:278px;background:#7c0006 url(http://cwsir.sinaapp.com/CWSirExtensions/images/banner.jpg) no-repeat center 0;} 19 | 20 | .main_wrap{background:url(http://cwsir.sinaapp.com/CWSirExtensions/images/bg_repeat.png) repeat-x 0 bottom;} 21 | .main_content{width:960px;margin:0 auto;} 22 | .main_content .tips{padding:30px 0;border-bottom:1px solid #e7e7e7;font-size:16px;line-height:30px;font-family:"Microsoft Yahei" "微软雅黑";} 23 | 24 | .function{padding-bottom:20px;border-bottom:1px solid #e3e3e3} 25 | .function li{float:left;width:480px;height:156px;overflow:hidden;} 26 | .function .f01{background:url(http://cwsir.sinaapp.com/CWSirExtensions/images/f01.jpg) no-repeat;} 27 | .function .f02{background:url(http://cwsir.sinaapp.com/CWSirExtensions/images/f02.jpg) no-repeat;} 28 | .function .f03{background:url(http://cwsir.sinaapp.com/CWSirExtensions/images/f03.jpg) no-repeat;} 29 | .function .f04{background:url(http://cwsir.sinaapp.com/CWSirExtensions/images/f04.jpg) no-repeat;} 30 | 31 | .download_wrap {padding:20px 0 0 0;font-size:0;} 32 | .download_wrap p{margin-bottom:30px;} 33 | .download_wrap .icon, 34 | .download_wrap .btn{vertical-align:middle;margin-right:15px;} 35 | .download_wrap .chrome_icon{display:inline-block;width:49px;height:50px;background:url(http://cwsir.sinaapp.com/CWSirExtensions/images/btns.png) no-repeat 0 -200px;} 36 | .download_wrap .chrome_cwsir{display:inline-block;width:183px;height:38px;background:url(http://cwsir.sinaapp.com/CWSirExtensions/images/btns.png) no-repeat 0 0;} 37 | .download_wrap .chrome_cwsir:hover{background-position:0 -50px;} 38 | .download_wrap .chrome_download{display:inline-block;width:109px;height:38px;background:url(http://cwsir.sinaapp.com/CWSirExtensions/images/btns.png) no-repeat -200px 0;margin-right:30px;} 39 | .download_wrap .chrome_download:hover{background-position:-200px -50px;} 40 | 41 | .download_wrap .s360_lighting_icon{display:inline-block;width:55px;height:55px;background:url(http://cwsir.sinaapp.com/CWSirExtensions/images/btns.png) no-repeat -60px -200px;} 42 | .download_wrap .s360_safe_icon{display:inline-block;width:60px;height:56px;background:url(http://cwsir.sinaapp.com/CWSirExtensions/images/btns.png) no-repeat -120px -200px;} 43 | 44 | .download_wrap .s360_cwsir{display:inline-block;width:155px;height:49px;background:url(http://cwsir.sinaapp.com/CWSirExtensions/images/btns.png) no-repeat 0 -100px;} 45 | .download_wrap .s360_cwsir:hover{background-position:0 -150px;} 46 | .download_wrap .s360_lighting{display:inline-block;width:103px;height:49px;background:url(http://cwsir.sinaapp.com/CWSirExtensions/images/btns.png) no-repeat -200px -100px;} 47 | .download_wrap .s360_lighting:hover{background-position:-200px -150px;} 48 | .download_wrap .s360_safe{display:inline-block;width:103px;height:49px;background:url(http://cwsir.sinaapp.com/CWSirExtensions/images/btns.png) no-repeat -318px -100px;margin-right:0;} 49 | .download_wrap .s360_safe:hover{background-position:-318px -150px;} 50 | 51 | .footer_wrap{background:#898989;box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.2);} 52 | .footer_wrap .footer{width:960px;margin:0 auto;padding:28px 0;} 53 | .footer_wrap .footer p{line-height:20px;color:#b7b6b6;} 54 | .footer_wrap .footer p a{color:#b7b6b6;} 55 | -------------------------------------------------------------------------------- /test/fixture/inline-comment.css: -------------------------------------------------------------------------------- 1 | /* csshint-disable min-font-size, no-bom */ /* csshint block-indent: [" ", 0] */ 2 | 3 | @keyframes errorbg { 4 | 0%, 5 | 25%, 6 | 100% { 7 | background: #f5cfc4; 8 | } 9 | } 10 | 11 | *body { 12 | _margin: 0; 13 | } 14 | 15 | aa { 16 | font-size: 0; 17 | } 18 | 19 | 20 | 21 | @media screen and (min-width:1024px) and (max-width:1280px) { 22 | body { 23 | font-size: medium; 24 | } 25 | } 26 | 27 | 28 | @media screen and (orientation:portrait) { 29 | @media screen and (min-device-width: 360px) { 30 | *body { 31 | margin-top: 5px; 32 | } 33 | } 34 | } 35 | 36 | @media handheld and (min-device-width: 10px) { 37 | body { 38 | *color: #f00; 39 | } 40 | } 41 | 42 | a { 43 | color: #fff; 44 | } 45 | 46 | -------------------------------------------------------------------------------- /test/fixture/leading-zero.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 0.5px; 3 | height: 10.10px; 4 | font-size: 12.5px; 5 | margin: 0.5px 1.2px 0.8px; 6 | color: #f00; 7 | text-transform: 13.0ms; 8 | } 9 | 10 | .for-test { 11 | color: rgba(0, 0, 0, 0.25); 12 | opacity: 0.6; 13 | transform: scale(0.5); 14 | } 15 | -------------------------------------------------------------------------------- /test/fixture/max-length.css: -------------------------------------------------------------------------------- 1 |  2 | #ccc .fff .qqq, #ccc .fff .qqq a, .aaaa, #ccc .fff a.cwsirrss_entry-source-title, #ccc .fff a.cwsirrss_entry-post-author-namecwsirrss_entry-post-author-name { 3 | color: #15c; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixture/max-length2.css: -------------------------------------------------------------------------------- 1 | #ccc .fff .qqq, #ccc .fff .qqq a, .aaaa, #ccc .fff a.cwsirrss_entry-source-title, #ccc .fff a.cwsirrss_entry-post-author-name { 2 | color: #15c;color: #151;color: #152;color: #153;color: #154;color: #155;color: #156;color: #157;color: #158;color: #159;color: #160; 3 | } 4 | 5 | div { 6 | background-image: 2px 2px url(data:image/gif;base64,R0lGODlhCAAHAIABAGZmZv///yH5BAEAAAEALAAAAAAIAAcAAAINjAOnkIr8mGxGOnhPAQA7); 7 | } 8 | -------------------------------------------------------------------------------- /test/fixture/max-selector-nesting-level.css: -------------------------------------------------------------------------------- 1 | body div span { 2 | color: red; 3 | } 4 | 5 | body > div > span > a, 6 | .aa .bb .cc .dd { 7 | color: green; 8 | } 9 | 10 | 11 | .page .header .login ~ .dsa { 12 | height: 100px; 13 | } 14 | 15 | p span { 16 | color: red; 17 | } 18 | -------------------------------------------------------------------------------- /test/fixture/min-font-size.css: -------------------------------------------------------------------------------- 1 | span { 2 | font-size: 11px;font-size: 11px; 3 | } 4 | 5 | div { 6 | font-size: 13px; 7 | } 8 | 9 | 10 | a { 11 | font-size:1px; 12 | } 13 | 14 | .dd { 15 | width: 5px; 16 | } 17 | 18 | .c { 19 | font-size: 1.2em; 20 | } 21 | -------------------------------------------------------------------------------- /test/fixture/no-bom.css: -------------------------------------------------------------------------------- 1 |  2 | #ccc .fff .qqq, #ccc .fff a.cwsirrss_entry-source-title, #ccc .fff a.cwsirrss_entry-post-author-name { 3 | color: #15c; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixture/omit-protocol-in-url.css: -------------------------------------------------------------------------------- 1 | span { 2 | background:#fff url("http://cwsir.sinaapp.com/banner.jpg") no-repeat center 0; 3 | background:#000 url('http://baidu.com/1.jpg') no-repeat url("http://asdasd.com/sda.png") 0; 4 | background:#aaa url(http://qq.com/2ee.jpg) no-repeat center 0; 5 | background:#aaa url(qq.com/2.jpg) no-repeat center 0; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixture/outline-none.css: -------------------------------------------------------------------------------- 1 | /* no :focus */ 2 | a { 3 | outline: none; 4 | } 5 | 6 | /* no :focus */ 7 | a { 8 | outline: 0; 9 | } 10 | 11 | /* :focus but missing a replacement treatment */ 12 | a:focus { 13 | outline: 0; 14 | } 15 | -------------------------------------------------------------------------------- /test/fixture/property-not-existed.css: -------------------------------------------------------------------------------- 1 | .aa { 2 | -webkit-border-radius: 5px; 3 | -moz-border-radius: 5px; 4 | -o-border-radius: 5px; 5 | border-radius: 5px; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixture/qualified-headings.css: -------------------------------------------------------------------------------- 1 | /* qualified heading */ 2 | .box h3 { 3 | font-weight: normal; 4 | } 5 | 6 | /* qualified heading */ 7 | .item:hover , 8 | .aaa h3 { 9 | font-weight: bold; 10 | } 11 | -------------------------------------------------------------------------------- /test/fixture/regex-selectors.css: -------------------------------------------------------------------------------- 1 | #mybox[class~="xxx"] .ccc[asd~="yyy"], 2 | #ll[class~="gg"] .bb[nn~="ii"] { 3 | color: red; 4 | } 5 | 6 | .mybox[class^=xxx] { 7 | color: red; 8 | } 9 | 10 | .mybox[class|=xxx] { 11 | color: red; 12 | } 13 | 14 | .mybox[class$=xxx] { 15 | color: red; 16 | } 17 | 18 | .mybox[class*=xxx] { 19 | color: red; 20 | } 21 | -------------------------------------------------------------------------------- /test/fixture/require-after-space.css: -------------------------------------------------------------------------------- 1 | span { 2 | *border-color:rgb(200,200,200);color: rgb(100,100,100); 3 | } 4 | 5 | div { 6 | background-image: 2px 2px url(data:image/gif;base64,R0lGODlhCAAHAIABAGZmZv///yH5BAEAAAEALAAAAAAIAAcAAAINjAOnkIr8mGxGOnhPAQA7); 7 | } 8 | -------------------------------------------------------------------------------- /test/fixture/require-after-space2.css: -------------------------------------------------------------------------------- 1 | /*.aaa .ddd::-webkit-scrollbar-thumb { 2 | background: rgba(0, 0, 0,0.6); 3 | } 4 | */ 5 | 6 | span { 7 | font-family: Arial,sans-serif,cas; 8 | color :red; 9 | color:yellow; 10 | color: green; 11 | } 12 | 13 | /* 14 | body,A { 15 | color : #CcccCd;font-size:0px; 16 | border-color:rgb(200,200,200); color:rgb(100, 100,100);} 17 | */ 18 | 19 | /*div { 20 | background-image: url(data:image/gif;base64,R0lGODlhCAAHAIABAGZmZv///yH5BAEAAAEALAAAAAAIAAcAAAINjAOnkIr8mGxGOnhPAQA7); 21 | }*/ 22 | -------------------------------------------------------------------------------- /test/fixture/require-around-space.css: -------------------------------------------------------------------------------- 1 | div ~ span >p { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixture/require-before-space.css: -------------------------------------------------------------------------------- 1 | 2 | .hetu_top_nav_body{ 3 | display: none; 4 | } 5 | 6 | asdasd { 7 | color: red; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixture/require-doublequotes.css: -------------------------------------------------------------------------------- 1 | /*html[lang|="zh"] q:before { 2 | font-family: "Microsoft YaHei", sans-serif; 3 | content: "“"; 4 | } 5 | */ 6 | /*html[lang|=zh] q:before { 7 | font-family: 'Microsoft YaHei', sans-serif; 8 | content: '“'; 9 | }*/ 10 | 11 | html[lang|='zh'] q:before { 12 | font-family: 'Microsoft YaHei', sans-serif; 13 | content: ''; 14 | background: #fff url("http://cwsir.sinaapp.com/banner.jpg") no-repeat center 0; 15 | } 16 | 17 | input[type=text] { 18 | width: 100px; 19 | content: "asdasd"; 20 | } 21 | 22 | .Table th[data-sort]:hover { 23 | cursor: pointer; 24 | } 25 | -------------------------------------------------------------------------------- /test/fixture/require-newline.css: -------------------------------------------------------------------------------- 1 | 2 | @media handheld and (min-width:360px), 3 | screen and (min-width:480px),screen and (max-width:980px) { 4 | body {font-size:large;} 5 | } 6 | 7 | @media screen and (min-width:1024px) and (max-width:1280px) { 8 | body {font-size:medium;} 9 | } 10 | 11 | @media screen and (min-width:800px),print and (min-width:7in) { 12 | body {font-size:small;} 13 | } 14 | 15 | span, label { 16 | color: #f00; 17 | } 18 | 19 | 20 | div, 21 | a:hover { 22 | color: #ccc; 23 | } 24 | 25 | 26 | span, 27 | label { 28 | color: #f00; 29 | } 30 | 31 | p { 32 | width: 10px;height: 30px; 33 | } 34 | 35 | 36 | p, 37 | i,.cc { 38 | width: 100px;height: 100px; 39 | } 40 | -------------------------------------------------------------------------------- /test/fixture/require-number.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-weight: bold; 3 | line-height: 50px; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixture/require-transition-property.css: -------------------------------------------------------------------------------- 1 | .box { 2 | transition: all 1s; 3 | transition: width 2s; 4 | transition: color 1s, sdall 10s, all 20s; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixture/reset.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | /*html,body{height:100%;}*/ 3 | /*html{filter:progid:DXImageTransform.Microsoft.BasicImage(grayscale=1);}*/ 4 | body, button, input, select, textarea { 5 | font: 12px / 1.125 Arial, Helvetica, sans-serif; 6 | font-family: "宋体"; 7 | } 8 | 9 | 10 | 11 | body, h1, h2, h3, h4, h5, h6, dl, dt, dd, ul, ol, li, th, td, p, blockquote, pre, form, fieldset, legend, input, button, textarea, hr, div { 12 | margin: 0; 13 | padding: 0; 14 | } 15 | 16 | table { 17 | border-collapse: collapse; 18 | border-spacing: 0; 19 | } 20 | 21 | li { 22 | list-style: none; 23 | } 24 | 25 | fieldset, img { 26 | border: 0; 27 | } 28 | 29 | q:before, q:after { 30 | content: ''; 31 | } 32 | 33 | input, textarea { 34 | outline-style: none; 35 | } 36 | 37 | input[type = 38 | "text"], input[type = "password"], textarea { 39 | outline-style: none; 40 | -webkit-appearance: none; 41 | } 42 | 43 | textarea { 44 | resize: none 45 | } 46 | 47 | address, caption, cite, code, dfn, em, i, th, var { 48 | font-style: normal; 49 | font-weight: normal; 50 | } 51 | 52 | legend { 53 | color: #000; 54 | } 55 | 56 | abbr, acronym { 57 | border: 0; 58 | font-variant: normal; 59 | } 60 | 61 | a { 62 | text-decoration: none; 63 | } 64 | 65 | a:hover { 66 | text-decoration: underline; 67 | } 68 | -------------------------------------------------------------------------------- /test/fixture/rss.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .cwsirrss_card { 4 | border-left-color: #4d90f0; 5 | } 6 | 7 | .cwsirrss_card { 8 | border: 1px solid #ddd; 9 | box-shadow: 3px 1px 4px #e3e5eb; 10 | } 11 | 12 | .cwsirrss_samedir .cwsirrss_entry .cwsirrss_card-common .cwsirrss_entry-container .cwsirrss_entry-date { 13 | float: right; 14 | } 15 | 16 | .cwsirrss_entry .cwsirrss_entry-title { 17 | font-size: 200%; 18 | margin-bottom: 10px; 19 | } 20 | 21 | .cwsirrss_item-body { 22 | font-size: 14px; 23 | line-height: 24px; 24 | max-height: 400px; 25 | word-wrap: break-word; 26 | overflow-x: hidden; 27 | overflow-y: auto; 28 | } 29 | 30 | .cwsirrss_item-body img { 31 | max-width: 500px !important; 32 | } 33 | 34 | .cwsirrss_card-common { 35 | margin: 20px; 36 | background-color: #fff; 37 | zoom: 1; 38 | } 39 | 40 | .cwsirrss_cards .cwsirrss_card-content { 41 | padding: 10px; 42 | } 43 | 44 | .cwsirrss_entry .cwsirrss_entry-container { 45 | position: relative; 46 | padding-bottom: .5em; 47 | zoom: 1; 48 | } 49 | 50 | .cwsirrss_entry .cwsirrss_entry-main { 51 | zoom: 1; 52 | overflow: hidden; 53 | } 54 | 55 | 56 | .cwsirrss_entry_bottom { 57 | padding: 10px 0 10px 20px; 58 | background-color: #ddd; 59 | } 60 | 61 | .cwsirrss_entry_share { 62 | float: right; 63 | padding: 0 20px 0 0; 64 | text-decoration: none; 65 | } 66 | 67 | .cwsirrss_entry_share_logo { 68 | background: url(http://cwsir.sinaapp.com/CWSirExtensions/images/sprite_ico.png) no-repeat; 69 | height: 16px; 70 | display: inline-block; 71 | width: 19px; 72 | background-position: 1px -697px; 73 | border: 0; 74 | } 75 | 76 | .cwsirrss_entry-icons-placeholder { 77 | display: inline-block; 78 | } 79 | 80 | #cwsirrss_entries .cwsirrss_entry-container .cwsirrss_entry-title, #cwsirrss_entries .cwsirrss_entry-container .cwsirrss_entry-title a, #cwsirrss_entries .cwsirrss_entry-container a.cwsirrss_entry-source-title, #cwsirrss_entries .cwsirrss_entry-container a.cwsirrss_entry-post-author-name { 81 | color: #15c; 82 | } 83 | 84 | .cwsirrss_entry .cwsirrss_entry-author, .cwsirrss_entry { 85 | color: #666; 86 | text-decoration: none; 87 | font-size: 115%; 88 | padding: 5px 0px; 89 | display: inline; 90 | } 91 | .cwsirrss_entry-date { 92 | padding: 10px 0 5px 10px; 93 | height: 5px; 94 | color: #666; 95 | text-decoration: none; 96 | font-size: 115%; 97 | padding: 5px 0px; 98 | display: inline-block; 99 | } 100 | 101 | #cwsirrss_entries.cwsirrss_single-source .cwsirrss_entry-source-title-parent, #cwsirrss_entries.cwsirrss_single-source .cwsirrss_entry-source-title { 102 | display: none; 103 | } 104 | 105 | #cwsirrss_entries .cwsirrss_entry-container .cwsirrss_entry-title, #cwsirrss_entries .cwsirrss_entry-container .cwsirrss_entry-title a, #cwsirrss_entries .cwsirrss_entry-container a.cwsirrss_entry-source-title, #cwsirrss_entries .cwsirrss_entry-container a.cwsirrss_entry-post-author-name { 106 | color: #15c; 107 | } 108 | 109 | #cwsirrss_entries.cwsirrss_read .cwsirrss_entry-container .cwsirrss_entry-body { 110 | color: #000; 111 | } 112 | 113 | .cwsirrss_read .cwsirrss_entry-container .cwsirrss_entry-body { 114 | color: #222; 115 | } 116 | 117 | .cwsirrss_entry .cwsirrss_entry-body { 118 | padding-top: .5em; 119 | } 120 | 121 | #cwsirrss_entries { 122 | margin: auto 20px 123 | } 124 | 125 | .cwsirrss_entry .cwsirrss_entry-body, .cwsirrss_entry .cwsirrss_entry-title { 126 | max-width: 650px; 127 | } 128 | 129 | /*scroll-bar*/ 130 | .cwsirrss_item-body::-webkit-scrollbar { 131 | width: 10px; 132 | } 133 | 134 | .cwsirrss_item-body::-webkit-scrollbar-track { 135 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 136 | -webkit-border-radius: 5px; 137 | border-radius: 5px; 138 | border: 1px solid #666666; 139 | } 140 | 141 | .cwsirrss_item-body::-webkit-scrollbar-thumb { 142 | -webkit-border-radius: 5px; 143 | border-radius: 5px; 144 | background: rgba(0, 0, 0, 0.6); 145 | -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5); 146 | } 147 | 148 | .cwsirrss_item-body::-webkit-scrollbar-thumb:window-inactive { 149 | background: rgba(0, 0, 0, 0.4); 150 | } 151 | -------------------------------------------------------------------------------- /test/fixture/shorthand.css: -------------------------------------------------------------------------------- 1 | #review-head { 2 | /*color: #a37e31; 3 | line-height: 60px; 4 | font-size: 1.5em; 5 | font-family: "Microsoft YaHei"; 6 | margin-top: 50px; 7 | margin-left: 85px;*/ 8 | /*color: #aaccaa;*/ 9 | /*border: 1px solid #ffffff #ccddcc;*/ 10 | line-height: 60px; 11 | font-size: 1.5em; 12 | font-family: "Microsoft YaHei"; 13 | } 14 | 15 | .aaa { 16 | display: block; 17 | padding: 0 15px 0 0px; 18 | width: 175px; 19 | font-family: "Microsoft YaHei"; 20 | } 21 | 22 | .bbb { 23 | display: inline; 24 | margin-bottom: 3px; 25 | font-size: 16px; 26 | line-height: 60; 27 | margin-top: 10px; 28 | margin-bottom: 10px; 29 | margin-left: 10px; 30 | margin-right: 10px; 31 | } 32 | -------------------------------------------------------------------------------- /test/fixture/star-property-hack.css: -------------------------------------------------------------------------------- 1 | .mybox { 2 | border: 1px solid black; 3 | *width: 100px; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixture/test.css: -------------------------------------------------------------------------------- 1 | /*.ani { 2 | -webkit-animation: spin 2s infinite linear; 3 | -moz-animation: spin 2s infinite linear; 4 | -ms-animation: spin 2s infinite linear; 5 | -o-animation: spin 2s infinite linear; 6 | animation: spin 2s infinite linear; 7 | color: #fff; 8 | } 9 | .cc { 10 | -webkit-filter: blur(11px); 11 | filter: blur(11px); 12 | } 13 | */ 14 | 15 | 16 | .wm-location-show-middle span { 17 | display: inline-block; 18 | margin-left: 4px; 19 | width: 11px; 20 | height: 11px; 21 | background:url(//m.baidu.com/static/search/appAla/weizhan/icon_how.png) no-repeat; 22 | background-size: 11px 11px; 23 | vertical-align: -1px; 24 | } -------------------------------------------------------------------------------- /test/fixture/text-indent.css: -------------------------------------------------------------------------------- 1 | /* missing direction */ 2 | .mybox { 3 | text-indent: -999px; 4 | } 5 | 6 | /* missing direction */ 7 | .mybox { 8 | text-indent: -999em; 9 | } 10 | 11 | /* direction is rtl */ 12 | .mybox { 13 | direction: rtl; 14 | text-indent: -999em; 15 | } 16 | -------------------------------------------------------------------------------- /test/fixture/underscore-property-hack.css: -------------------------------------------------------------------------------- 1 | .mybox { 2 | border: 1px solid black; 3 | _width: 100px; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixture/unicode.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecomfe/node-csshint/e4b57cc394e2809bba24aa35b7d398703d4d80b2/test/fixture/unicode.css -------------------------------------------------------------------------------- /test/fixture/unifying-color-case-sensitive.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #fff; 3 | color: #FFF; 4 | color: #000abC; 5 | color: #eeeeee; 6 | color: #CCC; 7 | border: 1px solid rgb(255, 255, 255); 8 | background: url("images/icons.png") #acacac no-repeat; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /test/fixture/unifying-font-family-case-sensitive.css: -------------------------------------------------------------------------------- 1 | 2 | h1 { 3 | font-family: arial, "Microsoft YaHei", sans-serif; 4 | } 5 | 6 | body { 7 | font-family: Arial, "Microsoft YaHei", sans-serif; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixture/unifying-font-family-case-sensitive1.css: -------------------------------------------------------------------------------- 1 | 2 | h1 { 3 | font-family: Arial, "Microsoft YaHei", sans-serif; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixture/unique-headings.css: -------------------------------------------------------------------------------- 1 | /* Two rules for h3 */ 2 | h3 { 3 | font-weight: normal; 4 | } 5 | 6 | .box h3 { 7 | font-weight: bold; 8 | } 9 | 10 | .cc h3:hover, 11 | 12 | .dd h3 { 13 | color: red; 14 | } 15 | 16 | .cc .cc { 17 | height: 100px; 18 | } 19 | -------------------------------------------------------------------------------- /test/fixture/universal-selector.css: -------------------------------------------------------------------------------- 1 | * { 2 | color: red; 3 | } 4 | 5 | .selected * { 6 | color: red; 7 | } 8 | 9 | .aaa .bbb > *, 10 | #ccc * { 11 | color: red; 12 | } 13 | 14 | #asdsadasd * { 15 | color: red; 16 | } 17 | -------------------------------------------------------------------------------- /test/fixture/unqualified-attributes.css: -------------------------------------------------------------------------------- 1 | [type=text] { 2 | color: red; 3 | } 4 | 5 | .selected [type=text] { 6 | color: red; 7 | } 8 | 9 | .selected ,.selectedasdsa [type=text] { 10 | color: red; 11 | } 12 | -------------------------------------------------------------------------------- /test/fixture/vendor-prefixes-sort.css: -------------------------------------------------------------------------------- 1 | .box { 2 | -moz-box-sizing: border-box; 3 | box-sizing: border-box; 4 | -webkit-box-sizing: border-box; 5 | 6 | } 7 | 8 | .radius { 9 | -webkit-border-radius: 5px; 10 | -moz-border-radius: 5px; 11 | border-radius: 5px; 12 | 13 | } 14 | 15 | .ani { 16 | -webkit-animation: spin 2s infinite linear; 17 | -moz-animation: spin 2s infinite linear; 18 | -ms-animation: spin 2s infinite linear; 19 | -o-animation: spin 2s infinite linear; 20 | animation: spin 2s infinite linear; 21 | color: #fff; 22 | } 23 | 24 | 25 | .shadow { 26 | position: absolute; 27 | width: 170px; 28 | background: #fefefe; 29 | color: #474848; 30 | font-size: 14px; 31 | left: -95px; 32 | top: -125px; 33 | border-radius: 5px; 34 | border-bottom: solid 1px #d0d0cf; 35 | -webkit-box-shadow: 0 0 4px #bebebe; 36 | box-shadow: 0 0 4px #bebebe; 37 | } 38 | 39 | .cc { 40 | -moz-animation: spin 2s infinite linear; 41 | animation: spin 2s infinite linear; 42 | border-radius: 5px; 43 | } 44 | 45 | 46 | .cc { 47 | -webkit-backface-visibility: hidden; 48 | backface-visibility: hidden; 49 | } 50 | -------------------------------------------------------------------------------- /test/fixture/zero-unit.css: -------------------------------------------------------------------------------- 1 | div { 2 | width: 0px ; 3 | background-position: 0px 0 1.2em 0px; 4 | height: 0em; 5 | aaa: 0s; 6 | qqq: 0%; 7 | bbb: 0ms; 8 | ccc: 0deg; 9 | ddd: 0%; 10 | padding: 0px 0px; 11 | } 12 | -------------------------------------------------------------------------------- /test/spec/checker.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file lib/checker.js 的测试用例 3 | * @author ielgnaw(wuji0223@gmail.com) 4 | */ 5 | 6 | import chai from 'chai'; 7 | import path from 'path'; 8 | import {readFileSync} from 'fs'; 9 | 10 | 'use strict'; 11 | 12 | let checker = require(path.join(__dirname, '../../lib', 'checker')); 13 | 14 | const expect = chai.expect; 15 | 16 | /* globals describe, it */ 17 | /* eslint-disable max-nested-callbacks */ 18 | describe('checker test suite\n', function () { 19 | this.timeout(50000); 20 | describe('checkString', () => { 21 | it('should return right length with maxError', () => { 22 | const filePath = 'path/to/file.css'; 23 | const fileContent = '/* csshint max-error: 0 */\np {\nheight: 0px\n}\n'; 24 | 25 | return checker.checkString(fileContent, filePath).then(invalidList => { 26 | if (invalidList && invalidList[0]) { 27 | expect(invalidList[0].messages.length).to.equal(3); 28 | } 29 | }); 30 | }); 31 | 32 | it('should return right result for inline-disable', () => { 33 | const filePath = 'path/to/file.css'; 34 | const fileContent = '/* csshint-disable: zero-unit */\np {\nheight: 0px;\n}\n'; 35 | 36 | return checker.checkString(fileContent, filePath).then(invalidList => { 37 | expect(invalidList.length).to.equal(1); 38 | expect(invalidList[0].messages[0].ruleName).to.not.equal('zero-unit'); 39 | }); 40 | }); 41 | 42 | it('should return right result for inline-disable all rules', () => { 43 | const filePath = 'path/to/file.css'; 44 | const fileContent = '/* csshint-disable */\np {\nheight: 0px;\n}\n'; 45 | 46 | return checker.checkString(fileContent, filePath).then(invalidList => { 47 | expect(invalidList.length).to.equal(0); 48 | }); 49 | }); 50 | 51 | it('should return right length', () => { 52 | const filePath = 'path/to/file.css'; 53 | const fileContent = '\np {\nheight: 0px\n}\n'; 54 | 55 | checker.checkString(fileContent, filePath).then(invalidList => { 56 | expect(3).to.equal(invalidList[0].messages.length); 57 | }); 58 | }); 59 | 60 | it('should return right errorChar', () => { 61 | const filePath = path.join(__dirname, '../fixture/error-char.css'); 62 | const fileContent = readFileSync(filePath, 'utf8').replace(/\r\n?/g, '\n'); 63 | 64 | checker.checkString(fileContent, filePath).then(invalidList => { 65 | const messages = invalidList[0].messages; 66 | expect(messages[0].errorChar).to.equal(':'); 67 | }); 68 | }); 69 | 70 | it('should catch error with line', () => { 71 | const filePath = 'path/to/file.css'; 72 | const fileContent = '\np {\nheight: 0px\n\n'; 73 | 74 | return checker.checkString(fileContent, filePath).then(() => { 75 | }, invalidList => { 76 | const messages = invalidList[0].messages; 77 | expect(messages[0].line).to.equal(2); 78 | }); 79 | }); 80 | }); 81 | 82 | describe('check', () => { 83 | it('should return right length', () => { 84 | const filePath = path.join(__dirname, '../fixture/test.css'); 85 | const fileContent = readFileSync( 86 | path.join(__dirname, '../fixture/test.css'), 87 | 'utf8' 88 | ).replace(/\r\n?/g, '\n'); 89 | 90 | const f = { 91 | path: filePath, 92 | content: fileContent 93 | }; 94 | 95 | const errors = []; 96 | 97 | return checker.check(f, errors, 98 | () => { 99 | expect(errors[0].messages.length).to.equal(1); 100 | } 101 | ); 102 | }); 103 | 104 | it('should be ignore', () => { 105 | const filePath = path.join(__dirname, '../fixture/csshintignore.css'); 106 | const fileContent = readFileSync( 107 | filePath, 108 | 'utf8' 109 | ).replace(/\r\n?/g, '\n'); 110 | 111 | const f = { 112 | path: filePath, 113 | content: fileContent 114 | }; 115 | 116 | const errors = []; 117 | 118 | return checker.check(f, errors, 119 | () => { 120 | expect(errors.length).to.equal(0); 121 | } 122 | ); 123 | }); 124 | }); 125 | }); 126 | /* eslint-enable max-nested-callbacks */ 127 | -------------------------------------------------------------------------------- /test/spec/prefixes.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file lib/prefixes.js的测试用例 3 | * @author ielgnaw(wuji0223@gmail.com) 4 | */ 5 | 6 | import chai from 'chai'; 7 | import path from 'path'; 8 | 9 | 'use strict'; 10 | 11 | let prefixes = require(path.join(__dirname, '../../lib', 'prefixes')); 12 | 13 | const expect = chai.expect; 14 | 15 | /* globals describe, it */ 16 | 17 | describe('prefixes test suite\n', () => { 18 | describe('prefixes', function () { 19 | it('should be a array', function () { 20 | expect(Object.prototype.toString.call(prefixes.getPrefixList())).to.equal('[object Array]'); 21 | }); 22 | 23 | it('should be a object', function () { 24 | expect(typeof prefixes.getPrefixMap()).to.equal('object'); 25 | }); 26 | }); 27 | }); 28 | --------------------------------------------------------------------------------