├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── test.yml ├── .gitignore ├── Changelog.md ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json └── test ├── fixtures ├── invalid.html ├── morethan16 │ ├── test1.html │ ├── test10.html │ ├── test11.html │ ├── test12.html │ ├── test13.html │ ├── test14.html │ ├── test15.html │ ├── test16.html │ ├── test17.html │ ├── test2.html │ ├── test3.html │ ├── test4.html │ ├── test5.html │ ├── test6.html │ ├── test7.html │ ├── test8.html │ └── test9.html ├── test-custom-rule │ ├── htmlhintrc.json │ ├── invalid-custom-rule-2.html │ ├── invalid-custom-rule.html │ └── valid-custom-rule.html └── valid.html ├── htmlhintrc.json └── main.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [test/fixtures/*] 16 | insert_final_newline = false 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: tests 5 | on: [push, pull_request] 6 | env: 7 | CI: true 8 | 9 | jobs: 10 | run: 11 | name: Node ${{ matrix.node-version }} on ${{ matrix.os }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | node-version: [10, 12, 14, 16] 17 | os: [ubuntu-latest, windows-latest] 18 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v2 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | cache: 'npm' 27 | 28 | - run: node --version 29 | - run: npm --version 30 | - name: Install npm dependencies 31 | run: npm ci 32 | 33 | - name: Run tests 34 | run: npm test 35 | 36 | - name: Run Coveralls 37 | uses: coverallsapp/github-action@master 38 | if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.node, '16') 39 | with: 40 | github-token: '${{ secrets.GITHUB_TOKEN }}' 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | temp/ 4 | .nyc_output 5 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | 2 | v2.2.1 / 2018-09-10 3 | =================== 4 | 5 | * Bump dependencies 6 | * Updated htmlhint dependency (#42) 7 | 8 | v2.1.1 / 2018-04-06 9 | ================== 10 | 11 | * Fix issue in specifying htmlhintrc in options. [#39] 12 | 13 | v2.1.0 / 2018-02-09 14 | =================== 15 | 16 | * Add support for custom rules 17 | 18 | 2.0.0 / 2018-01-23 19 | ================== 20 | 21 | * Drop node <6 support 22 | 23 | 1.0.0 / 2017-10-26 24 | ================== 25 | 26 | * Drop node <4 support 27 | * feat(reporters): failAfterError and failOnError (#32) 28 | * Add link for `gulp-reporter` (#30) 29 | * Merge pull request #26 from Titiaiev/patch-1 30 | * Update README.md 31 | * Merge pull request #19 from appfeel/master 32 | * Allow reporter to get options 33 | 34 | 0.3.0 / 2015-07-18 35 | ================== 36 | 37 | * Merge pull request #15 from doshprompt/require-reporter 38 | * Merge pull request #16 from doshprompt/fail-reporter 39 | * feat(failReporter): use suppress=true instead of errors=false 40 | * Update README.md 41 | * chore(README): more details on failReporter 42 | * feat(failReporter): add ability to turn off file errors on failure 43 | * test(reporter): load custom reporter by package name 44 | * feat(reporter): custom reporter can be loaded by its package name 45 | 46 | 0.2.1 / 2015-07-17 47 | ================== 48 | 49 | * feat(htmlhintrc): allow comments similar to jshintrc 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Ben Zörb 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gulp-htmlhint [![NPM version][npm-image]][npm-url] [![Build Status][ci-image]][ci-url] 2 | 3 | > [htmlhint](https://github.com/yaniswang/HTMLHint) wrapper for [gulp](https://github.com/wearefractal/gulp) to validate your HTML 4 | 5 | 6 | ## Usage 7 | 8 | First, install `gulp-htmlhint` as a development dependency: 9 | 10 | ```shell 11 | npm install --save-dev gulp-htmlhint 12 | ``` 13 | 14 | Then, add it to your `gulpfile.js`: 15 | 16 | ```javascript 17 | var htmlhint = require("gulp-htmlhint"); 18 | 19 | gulp.src("./src/*.html") 20 | .pipe(htmlhint()) 21 | ``` 22 | 23 | 24 | 25 | ## API 26 | 27 | ### htmlhint([options [, customRules]]) 28 | 29 | #### options 30 | See all rules here: [https://github.com/HTMLHint/HTMLHint/wiki/Rules](https://github.com/yaniswang/HTMLHint/wiki/Rules) 31 | 32 | If `options` is empty, the task will use standard options. 33 | 34 | ##### options.htmlhintrc 35 | Type: `String`
36 | Default value: `null` 37 | 38 | If this filename is specified, options and globals defined there will be used. Task and target options override the options within the `htmlhintrc` file. The `htmlhintrc` file must be valid JSON and looks something like this: 39 | 40 | ```json 41 | { 42 | "tag-pair": true 43 | } 44 | ``` 45 | 46 | ```javascript 47 | var htmlhint = require("gulp-htmlhint"); 48 | 49 | gulp.src("./src/*.html") 50 | .pipe(htmlhint('.htmlhintrc')) 51 | ``` 52 | 53 | #### customRules 54 | 55 | Type: `Array` _Optional_
56 | Default value: `null` 57 | 58 | Array that contains all user-defined custom rules. Rules added to this param need not exist in the `htmlhintrc` file. 59 | All rules inside this array should be valid objects and look like this: 60 | 61 | ```javascript 62 | { 63 | id: 'my-custom-rule', 64 | description: 'Custom rule definition', 65 | init: function(parser, reporter){ 66 | //Code goes here 67 | } 68 | } 69 | ``` 70 | 71 | Here is an example: 72 | 73 | ```javascript 74 | var htmlhint = require("gulp-htmlhint"); 75 | 76 | var customRules = []; 77 | customRules.push({ 78 | id: 'my-custom-rule', 79 | description: 'Custom rule definition', 80 | init: function(parser, reporter){ 81 | //Code goes here 82 | } 83 | }); 84 | 85 | gulp.src("./src/*.html") 86 | .pipe(htmlhint('.htmlhintrc', customRules)) 87 | ``` 88 | 89 | Note: You can call `htmlhint` function four different ways: 90 | 91 | - Without params (task will use standard options). 92 | - With `options` param alone. 93 | - With `customRules` param alone (task will only use custom rules options). 94 | - With both `options` and `customRules` params defined. 95 | 96 | ## Reporters 97 | 98 | ### Default reporter 99 | ```javascript 100 | var htmlhint = require("gulp-htmlhint"); 101 | 102 | gulp.src("./src/*.html") 103 | .pipe(htmlhint()) 104 | .pipe(htmlhint.reporter()) 105 | ``` 106 | 107 | 108 | ### Fail reporters 109 | 110 | #### failOnError 111 | 112 | Use this reporter if you want your task to fail on the first file that triggers an HTMLHint Error. 113 | It also prints a summary of all errors in the first bad file. 114 | 115 | ```javascript 116 | var htmlhint = require("gulp-htmlhint"); 117 | 118 | gulp.src("./src/*.html") 119 | .pipe(htmlhint()) 120 | .pipe(htmlhint.failOnError()) 121 | ``` 122 | 123 | #### failAfterError 124 | 125 | Use this reporter if you want to collect statistics from all files before failing. 126 | It also prints a summary of all errors in the first bad file. 127 | 128 | ```javascript 129 | var htmlhint = require("gulp-htmlhint"); 130 | 131 | gulp.src("./src/*.html") 132 | .pipe(htmlhint()) 133 | .pipe(htmlhint.failAfterError()) 134 | ``` 135 | 136 | #### Reporter options 137 | 138 | Optionally, you can pass a config object to either fail reporter. 139 | 140 | ##### suppress 141 | Type: `Boolean`
142 | Default value: `false` 143 | 144 | When set to `true`, errors are not displayed on failure. 145 | Use in conjunction with the default and/or custom reporter(s). 146 | Prevents duplication of error messages when used along with another reporter. 147 | 148 | ```javascript 149 | var htmlhint = require("gulp-htmlhint"); 150 | 151 | gulp.src("./src/*.html") 152 | .pipe(htmlhint()) 153 | .pipe(htmlhint.reporter("htmlhint-stylish")) 154 | .pipe(htmlhint.failOnError({ suppress: true })) 155 | ``` 156 | 157 | ### Third-party reporters 158 | 159 | [gulp-reporter](https://github.com/gucong3000/gulp-reporter) used in team project, it fails only when error belongs to the current author of git. 160 | 161 | ## License 162 | 163 | [MIT License](bezoerb.mit-license.org) 164 | 165 | [npm-url]: https://npmjs.org/package/gulp-htmlhint 166 | [npm-image]: https://badge.fury.io/js/gulp-htmlhint.svg 167 | 168 | [ci-url]: https://github.com/bezoerb/gulp-htmlhint/actions/workflows/test.yml 169 | [ci-image]: https://github.com/bezoerb/gulp-htmlhint/actions/workflows/test.yml/badge.svg 170 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const os = require('os'); 3 | const beep = require('beeper'); 4 | const c = require('ansi-colors'); 5 | const flog = require('fancy-log'); 6 | const through2 = require('through2'); 7 | const PluginError = require('plugin-error'); 8 | const stripJsonComments = require('strip-json-comments'); 9 | const {HTMLHint} = require('htmlhint'); 10 | 11 | const formatOutput = function (report, file, options) { 12 | 'use strict'; 13 | if (report.length === 0) { 14 | return { 15 | success: true 16 | }; 17 | } 18 | 19 | const filePath = (file.path || 'stdin'); 20 | 21 | // Handle errors 22 | const messages = report.filter(err => { 23 | return err; 24 | }).map(err => { 25 | return { 26 | file: filePath, 27 | error: err 28 | }; 29 | }); 30 | 31 | const output = { 32 | errorCount: messages.length, 33 | success: false, 34 | messages, 35 | options 36 | }; 37 | 38 | return output; 39 | }; 40 | 41 | const htmlhintPlugin = function (options, customRules) { 42 | 'use strict'; 43 | 44 | const ruleset = {}; 45 | 46 | if (!customRules && options && Array.isArray(options) && options.length > 0) { 47 | customRules = options; 48 | options = {}; 49 | } 50 | 51 | if (!options) { 52 | options = {}; 53 | } 54 | 55 | // Read Htmlhint options from a specified htmlhintrc file. 56 | if (typeof options === 'string') { 57 | // Don't catch readFile errors, let them bubble up 58 | options = { 59 | htmlhintrc: options 60 | }; 61 | } 62 | 63 | // If necessary check for required param(s), e.g. options hash, etc. 64 | // read config file for htmlhint if available 65 | if (options.htmlhintrc) { 66 | try { 67 | const externalOptions = fs.readFileSync(options.htmlhintrc, 'utf-8'); 68 | options = JSON.parse(stripJsonComments(externalOptions)); 69 | } catch (error) { 70 | throw new Error('gulp-htmlhint: Cannot parse .htmlhintrc ' + (error.message || error)); 71 | } 72 | } 73 | 74 | if (Object.keys(options).length > 0) { 75 | // Build a list of all available rules 76 | for (const key in HTMLHint.defaultRuleset) { 77 | if (HTMLHint.defaultRuleset.hasOwnProperty(key)) { // eslint-disable-line no-prototype-builtins 78 | ruleset[key] = 1; 79 | } 80 | } 81 | 82 | // Normalize htmlhint options 83 | // htmlhint only checks for rulekey, so remove rule if set to false 84 | for (const rule in options) { 85 | if (options[rule]) { 86 | ruleset[rule] = options[rule]; 87 | } else { 88 | delete ruleset[rule]; 89 | } 90 | } 91 | } 92 | 93 | // Add the defined custom rules 94 | // This will not require adding the costume rule id to the .htmlhintrc file 95 | if (customRules !== null && Array.isArray(customRules) && customRules.length > 0) { 96 | const has = Object.prototype.hasOwnProperty; 97 | for (const rule of customRules) { 98 | if (typeof rule === 'object') { 99 | HTMLHint.addRule(rule); 100 | if (has.call(rule, 'id')) { 101 | ruleset[rule.id] = true; 102 | } 103 | } 104 | } 105 | } 106 | 107 | return through2.obj((file, enc, cb) => { 108 | const report = HTMLHint.verify(file.contents.toString(), ruleset); 109 | 110 | // Send status down-stream 111 | file.htmlhint = formatOutput(report, file, options); 112 | cb(null, file); 113 | }); 114 | }; 115 | 116 | function getMessagesForFile(file) { 117 | 'use strict'; 118 | return file.htmlhint.messages.map(message_ => { 119 | const {error: message} = message_; 120 | let {evidence} = message; 121 | const {line, col} = message; 122 | const detail = line ? c.yellow('L' + line) + c.red(':') + c.yellow('C' + col) : c.yellow('GENERAL'); 123 | 124 | if (col === 0) { 125 | evidence = c.red('?') + evidence; 126 | } else if (col > evidence.length) { 127 | evidence = c.red(evidence + ' '); 128 | } else { 129 | evidence = evidence.slice(0, col - 1) + c.red(evidence[col - 1]) + evidence.slice(col); 130 | } 131 | 132 | return { 133 | message: c.red('[') + detail + c.red(']') + c.yellow(' ' + message.message) + ' (' + message.rule.id + ')', 134 | evidence 135 | }; 136 | }); 137 | } 138 | 139 | const defaultReporter = function (file) { 140 | 'use strict'; 141 | const {errorCount} = file.htmlhint; 142 | const plural = errorCount === 1 ? '' : 's'; 143 | 144 | beep(); 145 | 146 | flog(c.cyan(errorCount) + ' error' + plural + ' found in ' + c.magenta(file.path)); 147 | 148 | getMessagesForFile(file).forEach(data => { 149 | flog(data.message); 150 | flog(data.evidence); 151 | }); 152 | }; 153 | 154 | htmlhintPlugin.addRule = function (rule) { 155 | 'use strict'; 156 | return HTMLHint.addRule(rule); 157 | }; 158 | 159 | htmlhintPlugin.reporter = function (customReporter, options) { 160 | 'use strict'; 161 | let reporter = defaultReporter; 162 | 163 | if (typeof customReporter === 'function') { 164 | reporter = customReporter; 165 | } 166 | 167 | if (typeof customReporter === 'string') { 168 | if (customReporter === 'fail' || customReporter === 'failOn') { 169 | return htmlhintPlugin.failOnError(); 170 | } 171 | 172 | if (customReporter === 'failAfter') { 173 | return htmlhintPlugin.failAfterError(); 174 | } 175 | 176 | reporter = require(customReporter); 177 | } 178 | 179 | if (typeof reporter === 'undefined') { 180 | throw new TypeError('Invalid reporter'); 181 | } 182 | 183 | return through2.obj((file, enc, cb) => { 184 | // Only report if HTMLHint ran and errors were found 185 | if (file.htmlhint && !file.htmlhint.success) { 186 | reporter(file, file.htmlhint.messages, options); 187 | } 188 | 189 | cb(null, file); 190 | }); 191 | }; 192 | 193 | htmlhintPlugin.failOnError = function (options) { 194 | 'use strict'; 195 | options = options || {}; 196 | return through2.obj((file, enc, cb) => { 197 | // Something to report and has errors 198 | let error; 199 | if (file.htmlhint && !file.htmlhint.success) { 200 | if (options.suppress === true) { 201 | error = new PluginError('gulp-htmlhint', { 202 | message: 'HTMLHint failed.', 203 | showStack: false 204 | }); 205 | } else { 206 | const {errorCount} = file.htmlhint; 207 | const plural = errorCount === 1 ? '' : 's'; 208 | const message = c.cyan(errorCount) + ' error' + plural + ' found in ' + c.magenta(file.path); 209 | const messages = [message].concat(getMessagesForFile(file).map(m => { 210 | return m.message; 211 | })); 212 | 213 | error = new PluginError('gulp-htmlhint', { 214 | message: messages.join(os.EOL), 215 | showStack: false 216 | }); 217 | } 218 | } 219 | 220 | cb(error, file); 221 | }); 222 | }; 223 | 224 | htmlhintPlugin.failAfterError = function (options) { 225 | 'use strict'; 226 | options = options || {}; 227 | let globalErrorCount = 0; 228 | let globalErrorMessage = ''; 229 | return through2.obj(check, summarize); 230 | 231 | function check(file, enc, cb) { 232 | if (file.htmlhint && !file.htmlhint.success) { 233 | if (options.suppress === true) { 234 | globalErrorCount += file.htmlhint.errorCount; 235 | } else { 236 | globalErrorCount += file.htmlhint.errorCount; 237 | const plural = file.htmlhint.errorCount === 1 ? '' : 's'; 238 | const message = c.cyan(file.htmlhint.errorCount) + ' error' + plural + ' found in ' + c.magenta(file.path); 239 | const messages = [message].concat(getMessagesForFile(file).map(m => { 240 | return m.message; 241 | })); 242 | 243 | globalErrorMessage += messages.join(os.EOL) + os.EOL; 244 | } 245 | } 246 | 247 | cb(null, file); 248 | } 249 | 250 | function summarize(cb) { 251 | if (!globalErrorCount) { 252 | cb(); 253 | return; 254 | } 255 | 256 | const plural = globalErrorCount === 1 ? '' : 's'; 257 | const message = globalErrorMessage ? 258 | c.cyan(globalErrorCount) + ' error' + plural + ' overall:' + os.EOL + globalErrorMessage : 259 | c.cyan(globalErrorCount) + ' error' + plural + ' overall.'; 260 | 261 | const error = new PluginError('gulp-htmlhint', { 262 | message: 'HTMLHint failed. ' + message, 263 | showStack: false 264 | }); 265 | cb(error); 266 | } 267 | }; 268 | 269 | // Backward compatibility 270 | htmlhintPlugin.failReporter = htmlhintPlugin.failOnError; 271 | 272 | module.exports = htmlhintPlugin; 273 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-htmlhint", 3 | "version": "4.0.2", 4 | "description": "A plugin for Gulp", 5 | "keywords": [ 6 | "gulpplugin" 7 | ], 8 | "homepage": "https://github.com/bezoerb/gulp-htmlhint", 9 | "bugs": { 10 | "url": "https://github.com/bezoerb/gulp-htmlhint/issues" 11 | }, 12 | "author": "Ben Zörb (https://github.com/bezoerb)", 13 | "main": "./index.js", 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/bezoerb/gulp-htmlhint.git" 17 | }, 18 | "scripts": { 19 | "coveralls": "nyc report --reporter=text-lcov | coveralls", 20 | "test": "xo && nyc mocha test/*.js" 21 | }, 22 | "dependencies": { 23 | "ansi-colors": "^4.1.1", 24 | "beeper": "^2.0.0", 25 | "fancy-log": "^1.3.2", 26 | "htmlhint": "^1.1.2", 27 | "plugin-error": "^1.0.1", 28 | "strip-ansi": "^6.0.0", 29 | "strip-json-comments": "^3.1.1", 30 | "through2": "^3.0.1", 31 | "vinyl": "^2.2.1" 32 | }, 33 | "devDependencies": { 34 | "coveralls": "^3.1.0", 35 | "htmlhint-stylish": "^1.0.3", 36 | "mocha": "^8.4.0", 37 | "nyc": "^14.1.1", 38 | "should": "^13.2.3", 39 | "vinyl-fs": "^3.0.3", 40 | "xo": "^0.36.1" 41 | }, 42 | "peerDependencies": { 43 | "htmlhint": "^0.14.0 || ^1.0.0" 44 | }, 45 | "xo": { 46 | "space": 2 47 | }, 48 | "engines": { 49 | "node": ">=10" 50 | }, 51 | "licenses": [ 52 | { 53 | "type": "MIT" 54 | } 55 | ], 56 | "directories": { 57 | "test": "test" 58 | }, 59 | "license": "MIT" 60 | } 61 | -------------------------------------------------------------------------------- /test/fixtures/invalid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 8 |

Test page 9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/morethan16/test1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 8 |

Test page

9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/morethan16/test10.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 8 |

Test page

9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/morethan16/test11.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 8 |

Test page

9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/morethan16/test12.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 8 |

Test page

9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/morethan16/test13.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 8 |

Test page

9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/morethan16/test14.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 8 |

Test page

9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/morethan16/test15.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 8 |

Test page

9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/morethan16/test16.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 8 |

Test page

9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/morethan16/test17.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 8 |

Test page

9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/morethan16/test2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 8 |

Test page

9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/morethan16/test3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 8 |

Test page

9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/morethan16/test4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 8 |

Test page

9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/morethan16/test5.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 8 |

Test page

9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/morethan16/test6.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 8 |

Test page

9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/morethan16/test7.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 8 |

Test page

9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/morethan16/test8.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 8 |

Test page

9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/morethan16/test9.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 8 |

Test page

9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/test-custom-rule/htmlhintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tag-pair": true 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/test-custom-rule/invalid-custom-rule-2.html: -------------------------------------------------------------------------------- 1 |

No match

2 |
3 | 4 |

First match 5 | 6 |

7 | -------------------------------------------------------------------------------- /test/fixtures/test-custom-rule/invalid-custom-rule.html: -------------------------------------------------------------------------------- 1 | 2 |

First match

3 |
4 | 5 |

Second match 6 | 7 |

8 |
9 | -------------------------------------------------------------------------------- /test/fixtures/test-custom-rule/valid-custom-rule.html: -------------------------------------------------------------------------------- 1 | 2 |

First match

3 |
4 |

Inside someDiv

5 |
6 |
7 | -------------------------------------------------------------------------------- /test/fixtures/valid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 8 |

Test page

9 | 10 | -------------------------------------------------------------------------------- /test/htmlhintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tag-pair": false 3 | } -------------------------------------------------------------------------------- /test/main.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | 'use strict'; 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const should = require('should'); 6 | const stripAnsi = require('strip-ansi'); 7 | const vfs = require('vinyl-fs'); 8 | const File = require('vinyl'); 9 | const {HTMLHint} = require('htmlhint'); 10 | const htmlhint = require('..'); 11 | 12 | const getFile = function (filePath) { 13 | filePath = 'test/' + filePath; 14 | return new File({ 15 | path: filePath, 16 | cwd: 'test/', 17 | base: path.dirname(filePath), 18 | contents: fs.readFileSync(filePath) 19 | }); 20 | }; 21 | 22 | // User defined custom rule for test 23 | const customRules = []; 24 | customRules.push({ 25 | id: 'require-some-tag', 26 | description: 'Require the presence of some-tag as the first element in files and not duplicated.', 27 | init(parser, reporter) { 28 | const self = this; 29 | let someTagOccurrences = 0; 30 | let someTagIsFirstElement = false; 31 | let iteration = 0; 32 | function onTagStart(event) { 33 | const tagName = event.tagName.toLowerCase(); 34 | if (tagName === 'some-tag' && iteration === 0) { 35 | someTagIsFirstElement = true; 36 | someTagOccurrences++; 37 | } else if (tagName === 'some-tag' && iteration > 0) { 38 | someTagOccurrences++; 39 | } 40 | 41 | if (!someTagIsFirstElement) { 42 | reporter.error('The tag must be present as first element.', event.line, event.col, self, event.raw); 43 | parser.removeListener('tagstart', onTagStart); 44 | } 45 | 46 | if (someTagOccurrences > 1) { 47 | reporter.error('The tag must be present only once.', event.line, event.col, self, event.raw); 48 | parser.removeListener('tagstart', onTagStart); 49 | } 50 | 51 | iteration++; 52 | } 53 | 54 | parser.addListener('tagstart', onTagStart); 55 | } 56 | }); 57 | 58 | describe('gulp-htmlhint', () => { 59 | it('should pass valid file', done => { 60 | let valid = 0; 61 | 62 | const fakeFile = getFile('fixtures/valid.html'); 63 | 64 | const stream = htmlhint(); 65 | 66 | stream.on('error', err => { 67 | should.not.exist(err); 68 | }); 69 | 70 | stream.on('data', file => { 71 | should.exist(file); 72 | file.htmlhint.success.should.equal(true); 73 | should.exist(file.path); 74 | should.exist(file.relative); 75 | should.exist(file.contents); 76 | ++valid; 77 | }); 78 | 79 | stream.once('end', () => { 80 | valid.should.equal(1); 81 | done(); 82 | }); 83 | 84 | stream.write(fakeFile); 85 | stream.end(); 86 | }); 87 | 88 | it('should fail invalid file', done => { 89 | let invalid = 0; 90 | 91 | const fakeFile = getFile('fixtures/invalid.html'); 92 | 93 | const stream = htmlhint(); 94 | 95 | stream.on('error', err => { 96 | should.not.exist(err); 97 | }); 98 | 99 | stream.on('data', file => { 100 | should.exist(file); 101 | file.htmlhint.success.should.equal(false); 102 | file.htmlhint.errorCount.should.equal(1); 103 | file.htmlhint.messages.length.should.equal(1); 104 | should.exist(file.path); 105 | should.exist(file.relative); 106 | should.exist(file.contents); 107 | ++invalid; 108 | }); 109 | 110 | stream.once('end', () => { 111 | invalid.should.equal(1); 112 | done(); 113 | }); 114 | 115 | stream.write(fakeFile); 116 | stream.end(); 117 | }); 118 | 119 | it('should lint two files', done => { 120 | let a = 0; 121 | 122 | const file1 = getFile('fixtures/valid.html'); 123 | const file2 = getFile('fixtures/invalid.html'); 124 | 125 | const stream = htmlhint(); 126 | stream.on('data', () => { 127 | ++a; 128 | }); 129 | 130 | stream.once('end', () => { 131 | a.should.equal(2); 132 | done(); 133 | }); 134 | 135 | stream.write(file1); 136 | stream.write(file2); 137 | stream.end(); 138 | }); 139 | 140 | it('should support options', done => { 141 | let a = 0; 142 | 143 | const file = getFile('fixtures/invalid.html'); 144 | 145 | const stream = htmlhint({ 146 | 'tag-pair': false 147 | }); 148 | stream.on('data', newFile => { 149 | ++a; 150 | should.exist(newFile.htmlhint.success); 151 | newFile.htmlhint.success.should.equal(true); 152 | should.not.exist(newFile.htmlhint.results); 153 | should.not.exist(newFile.htmlhint.options); 154 | }); 155 | stream.once('end', () => { 156 | a.should.equal(1); 157 | done(); 158 | }); 159 | 160 | stream.write(file); 161 | stream.end(); 162 | }); 163 | 164 | it('should support htmlhintrc', done => { 165 | let a = 0; 166 | 167 | const file = getFile('fixtures/invalid.html'); 168 | 169 | const stream = htmlhint('./test/htmlhintrc.json'); 170 | stream.on('data', newFile => { 171 | ++a; 172 | should.exist(newFile.htmlhint.success); 173 | newFile.htmlhint.success.should.equal(true); 174 | should.not.exist(newFile.htmlhint.results); 175 | should.not.exist(newFile.htmlhint.options); 176 | }); 177 | stream.once('end', () => { 178 | a.should.equal(1); 179 | done(); 180 | }); 181 | 182 | stream.write(file); 183 | stream.end(); 184 | }); 185 | 186 | it('should emit error on failure', done => { 187 | const file = getFile('fixtures/invalid.html'); 188 | 189 | const stream = htmlhint(); 190 | 191 | const failStream = htmlhint.reporter('fail'); 192 | stream.pipe(failStream); 193 | 194 | failStream.on('error', err => { 195 | should.exist(err); 196 | err.message.indexOf(file.relative).should.not.equal(-1, 'should say which file'); 197 | done(); 198 | }); 199 | 200 | stream.write(file); 201 | stream.end(); 202 | }); 203 | 204 | it('should add a htmlhint rule', () => { 205 | const fakeRule = { 206 | id: 'foo', 207 | description: 'bar', 208 | init() {} 209 | }; 210 | htmlhint.addRule(fakeRule); 211 | HTMLHint.rules[fakeRule.id].should.equal(fakeRule); 212 | }); 213 | }); 214 | 215 | describe('htmlhint.reporter', () => { 216 | it('should not fail for more than 16 files', done => { 217 | let a = 0; 218 | 219 | const stream = vfs.src('test/fixtures/morethan16/*.html') 220 | .pipe(htmlhint()) 221 | .pipe(htmlhint.reporter(() => { 222 | a++; 223 | })); 224 | 225 | stream.on('data', () => { 226 | }); 227 | 228 | stream.once('end', () => { 229 | a.should.equal(17); 230 | done(); 231 | }); 232 | }); 233 | 234 | it('should load custom reporters by package name', done => { 235 | let valid = 0; 236 | 237 | const stream = vfs.src('test/fixtures/valid.html') 238 | .pipe(htmlhint()) 239 | .pipe(htmlhint.reporter('htmlhint-stylish')); 240 | 241 | stream.on('error', err => { 242 | should.not.exist(err); 243 | }); 244 | 245 | stream.on('data', file => { 246 | should.exist(file); 247 | /* eslint no-unused-expressions: 0 */ 248 | file.htmlhint.success.should.be.true; 249 | should.exist(file.path); 250 | should.exist(file.relative); 251 | should.exist(file.contents); 252 | ++valid; 253 | }); 254 | 255 | stream.once('end', () => { 256 | valid.should.equal(1); 257 | done(); 258 | }); 259 | }); 260 | }); 261 | 262 | describe('htmlhint.failOnError', () => { 263 | it('should throw an error when using on an invalid file', done => { 264 | let error = false; 265 | const stream = vfs.src('test/fixtures/invalid.html') 266 | .pipe(htmlhint()) 267 | .pipe(htmlhint.failOnError()); 268 | 269 | stream.on('error', err => { 270 | error = true; 271 | stripAnsi(err.message).should.containEql('[L9:C1] Tag must be paired, missing: [ ], start tag match failed [

]'); 272 | err.name.should.equal('Error'); 273 | done(); 274 | }); 275 | 276 | stream.once('end', () => { 277 | /* eslint no-unused-expressions: 0 */ 278 | error.should.be.true; 279 | done(); 280 | }); 281 | }); 282 | 283 | it('should throw an error (from one file) when using more than one file', done => { 284 | let error = false; 285 | const stream = vfs.src('test/fixtures/morethan16/*.html') 286 | .pipe(htmlhint()) 287 | .pipe(htmlhint.failOnError()); 288 | 289 | stream.on('error', err => { 290 | error = true; 291 | stripAnsi(err.message).should.containEql('[L9:C1] Tag must be paired, missing: [

], start tag match failed [

]'); 292 | err.name.should.equal('Error'); 293 | done(); 294 | }); 295 | 296 | stream.once('end', () => { 297 | /* eslint no-unused-expressions: 0 */ 298 | error.should.be.true; 299 | done(); 300 | }); 301 | }); 302 | 303 | it('should not show file errors if suppress option is explicitly set', done => { 304 | let error = false; 305 | const stream = vfs.src('test/fixtures/invalid.html') 306 | .pipe(htmlhint()) 307 | .pipe(htmlhint.failOnError({ 308 | suppress: true 309 | })); 310 | 311 | stream.on('error', err => { 312 | error = true; 313 | stripAnsi(err.message).should.containEql('HTMLHint failed.'); 314 | err.name.should.equal('Error'); 315 | done(); 316 | }); 317 | 318 | it('should throw an error when using on an invalid file', done => { 319 | let error = false; 320 | const stream = vfs.src('test/fixtures/invalid.html') 321 | .pipe(htmlhint()) 322 | .pipe(htmlhint.failOnError()); 323 | 324 | stream.on('error', err => { 325 | error = true; 326 | stripAnsi(err.message).should.containEql('[L9:C1] Tag must be paired, missing: [

], start tag match failed [

]'); 327 | err.name.should.equal('Error'); 328 | done(); 329 | }); 330 | 331 | stream.once('end', () => { 332 | /* eslint no-unused-expressions: 0 */ 333 | error.should.be.true; 334 | done(); 335 | }); 336 | }); 337 | 338 | stream.once('end', () => { 339 | /* eslint no-unused-expressions: 0 */ 340 | error.should.be.true; 341 | done(); 342 | }); 343 | }); 344 | }); 345 | 346 | describe('htmlhint.failReporter - backward compatibility', () => { 347 | it('should throw an error when using on an invalid file', done => { 348 | let error = false; 349 | const stream = vfs.src('test/fixtures/invalid.html') 350 | .pipe(htmlhint()) 351 | .pipe(htmlhint.failReporter()); 352 | 353 | stream.on('error', err => { 354 | error = true; 355 | stripAnsi(err.message).should.containEql('[L9:C1] Tag must be paired, missing: [

], start tag match failed [

]'); 356 | err.name.should.equal('Error'); 357 | done(); 358 | }); 359 | 360 | stream.once('end', () => { 361 | /* eslint no-unused-expressions: 0 */ 362 | error.should.be.true; 363 | done(); 364 | }); 365 | }); 366 | 367 | it('should throw an error (from one file) when using more than one file', done => { 368 | let error = false; 369 | const stream = vfs.src('test/fixtures/morethan16/*.html') 370 | .pipe(htmlhint()) 371 | .pipe(htmlhint.failReporter()); 372 | 373 | stream.on('error', err => { 374 | error = true; 375 | stripAnsi(err.message).should.containEql('[L9:C1] Tag must be paired, missing: [

], start tag match failed [

]'); 376 | err.name.should.equal('Error'); 377 | done(); 378 | }); 379 | 380 | stream.once('end', () => { 381 | /* eslint no-unused-expressions: 0 */ 382 | error.should.be.true; 383 | done(); 384 | }); 385 | }); 386 | 387 | it('should not show file errors if suppress option is explicitly set', done => { 388 | let error = false; 389 | const stream = vfs.src('test/fixtures/invalid.html') 390 | .pipe(htmlhint()) 391 | .pipe(htmlhint.failReporter({ 392 | suppress: true 393 | })); 394 | 395 | stream.on('error', err => { 396 | error = true; 397 | stripAnsi(err.message).should.containEql('HTMLHint failed.'); 398 | err.name.should.equal('Error'); 399 | done(); 400 | }); 401 | 402 | stream.once('end', () => { 403 | /* eslint no-unused-expressions: 0 */ 404 | error.should.be.true; 405 | done(); 406 | }); 407 | }); 408 | }); 409 | 410 | describe('htmlhint.failAfterError', () => { 411 | it('should throw an error when using on an invalid file', done => { 412 | let error = false; 413 | const stream = vfs.src('test/fixtures/invalid.html') 414 | .pipe(htmlhint()) 415 | .pipe(htmlhint.failAfterError()); 416 | 417 | stream.on('error', err => { 418 | error = true; 419 | stripAnsi(err.message).should.containEql('[L9:C1] Tag must be paired, missing: [

], start tag match failed [

]'); 420 | err.name.should.equal('Error'); 421 | done(); 422 | }); 423 | 424 | stream.once('end', () => { 425 | /* eslint no-unused-expressions: 0 */ 426 | error.should.be.true; 427 | done(); 428 | }); 429 | }); 430 | 431 | it('should throw an error (from all files) when using more than one file', done => { 432 | let error = false; 433 | const stream = vfs.src(['test/fixtures/morethan16/test1.html', 'test/fixtures/morethan16/test2.html']) 434 | .pipe(htmlhint()) 435 | .pipe(htmlhint.failAfterError()); 436 | 437 | stream.on('error', err => { 438 | error = true; 439 | stripAnsi(err.message).should.containEql('HTMLHint failed. 4 errors overall:'); 440 | stripAnsi(err.message).should.containEql(path.normalize('morethan16/test1.html')); 441 | stripAnsi(err.message).should.containEql(path.normalize('morethan16/test2.html')); 442 | stripAnsi(err.message).should.containEql('[L9:C1] Tag must be paired, missing: [

], start tag match failed [

]'); 443 | err.name.should.equal('Error'); 444 | done(); 445 | }); 446 | 447 | stream.once('end', () => { 448 | /* eslint no-unused-expressions: 0 */ 449 | error.should.be.true; 450 | done(); 451 | }); 452 | }); 453 | 454 | it('should not show file errors if suppress option is explicitly set', done => { 455 | let error = false; 456 | const stream = vfs.src('test/fixtures/invalid.html') 457 | .pipe(htmlhint()) 458 | .pipe(htmlhint.failAfterError({ 459 | suppress: true 460 | })); 461 | 462 | stream.on('error', err => { 463 | error = true; 464 | stripAnsi(err.message).should.containEql('HTMLHint failed. 1 error overall.'); 465 | err.name.should.equal('Error'); 466 | done(); 467 | }); 468 | 469 | stream.once('end', () => { 470 | /* eslint no-unused-expressions: 0 */ 471 | error.should.be.true; 472 | done(); 473 | }); 474 | }); 475 | }); 476 | 477 | describe('customRules with htmlhintrc', () => { 478 | it('should throw an error when some-tag is not the first element', done => { 479 | let error = false; 480 | const stream = vfs.src('test/fixtures/test-custom-rule/invalid-custom-rule-2.html') 481 | .pipe(htmlhint('test/fixtures/test-custom-rule/htmlhintrc.json', customRules)) 482 | .pipe(htmlhint.failAfterError()); 483 | 484 | stream.on('error', err => { 485 | error = true; 486 | stripAnsi(err.message).should.containEql('[L1:C1] The tag must be present as first element.'); 487 | stripAnsi(err.message).should.containEql('[L5:C2] Tag must be paired, missing: [

]'); 488 | err.name.should.equal('Error'); 489 | done(); 490 | }); 491 | 492 | stream.once('end', () => { 493 | /* eslint no-unused-expressions: 0 */ 494 | error.should.be.true; 495 | done(); 496 | }); 497 | }); 498 | 499 | it('should throw an error when some-tag is defined more than once', done => { 500 | let error = false; 501 | const stream = vfs.src('test/fixtures/test-custom-rule/invalid-custom-rule.html') 502 | .pipe(htmlhint('test/fixtures/test-custom-rule/htmlhintrc.json', customRules)) 503 | .pipe(htmlhint.failAfterError()); 504 | 505 | stream.on('error', err => { 506 | error = true; 507 | stripAnsi(err.message).should.containEql('[L4:C3] The tag must be present only once.'); 508 | stripAnsi(err.message).should.containEql('[L6:C3] Tag must be paired, missing: [ ]'); 509 | err.name.should.equal('Error'); 510 | done(); 511 | }); 512 | 513 | stream.once('end', () => { 514 | /* eslint no-unused-expressions: 0 */ 515 | error.should.be.true; 516 | done(); 517 | }); 518 | }); 519 | }); 520 | 521 | describe('customRules', () => { 522 | it('should throw an error when some-tag is not the first element', done => { 523 | let error = false; 524 | const stream = vfs.src('test/fixtures/test-custom-rule/invalid-custom-rule-2.html') 525 | .pipe(htmlhint(customRules)) 526 | .pipe(htmlhint.failAfterError()); 527 | 528 | stream.on('error', err => { 529 | error = true; 530 | stripAnsi(err.message).should.containEql('[L1:C1] The tag must be present as first element.'); 531 | err.name.should.equal('Error'); 532 | done(); 533 | }); 534 | 535 | stream.once('end', () => { 536 | /* eslint no-unused-expressions: 0 */ 537 | error.should.be.true; 538 | done(); 539 | }); 540 | }); 541 | 542 | it('should throw an error when some-tag is defined more than once', done => { 543 | let error = false; 544 | const stream = vfs.src('test/fixtures/test-custom-rule/invalid-custom-rule.html') 545 | .pipe(htmlhint('test/htmlhintrc.json', customRules)) 546 | .pipe(htmlhint.failAfterError()); 547 | 548 | stream.on('error', err => { 549 | error = true; 550 | stripAnsi(err.message).should.containEql('[L4:C3] The tag must be present only once.'); 551 | err.name.should.equal('Error'); 552 | done(); 553 | }); 554 | 555 | stream.once('end', () => { 556 | /* eslint no-unused-expressions: 0 */ 557 | error.should.be.true; 558 | done(); 559 | }); 560 | }); 561 | }); 562 | --------------------------------------------------------------------------------