├── .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 |