poppinlp/grunt-htmlhint-plus

View on GitHub
tasks/htmlhintplus.js

Summary

Maintainability
D
1 day
Test Coverage
'use strict';

module.exports = function (grunt) {
    grunt.registerMultiTask('htmlhintplus', 'Validate html files with htmlhint.', function () {
        let
            fs = require('fs'),
            HTMLHint = require("htmlhint").HTMLHint,
            Fc = require('file-changed'),
            _ = require('lodash'),
            checkstyleFormatter = require('checkstyle-formatter'),
            defaultRules = {
                "tagname-lowercase": true,
                "attr-lowercase": true,
                "attr-value-double-quotes": true,
                "attr-value-not-empty": true,
                "attr-no-duplication": true,
                "doctype-first": true,
                "tag-pair": true,
                "tag-self-close": false,
                "spec-char-escape": true,
                "id-unique": true,
                "src-not-empty": true,
                "head-script-disabled": false,
                "img-alt-require": true,
                "doctype-html5": true,
                "id-class-value": "dash",
                "style-disabled": false,
                "space-tab-mixed-disabled": true,
                "id-class-ad-disabled": true,
                "href-abs-or-rel": true,
                "attr-unsafe-chars": true
            },
            options = this.options(),
            hasError = false,
            customRules = [],
            reducedResults = [],
            extendRules = options.extendRules || false,
            outputTypes = options.output ? (Array.isArray(options.output) ? options.output : options.output.split('|')) : ['console'],
            fc = new Fc();

        outputTypes = outputTypes.map(type => type.toString().toLowerCase());

        if (options.hasOwnProperty('customRules') && typeof options.customRules == 'object') {
            customRules = options.customRules;
        }

        // load custom rules
        if (customRules.length) {
            for (var i = 0, len = customRules.length; i < len; i++) {
                var customRule = require(process.cwd() + '/' + customRules[i]);
                if (typeof customRule == 'function') {
                    customRule(HTMLHint);
                } else if (
                    typeof customRule == 'object' &&
                    customRule.hasOwnProperty('id') &&
                    customRule.hasOwnProperty('description') &&
                    customRule.hasOwnProperty('init')
                ) {
                    HTMLHint.addRule(customRule);
                } else {
                    console.error("[error]".red.bold, customRules[i].bold, "was invalid and could not be loaded.".red);
                }
            }
        }

        this.files.map(function (files) {
            files.src.map(function (file) {
                if (options.newer && fc.addFile(file).check(file).length === 0) return;

                var text = grunt.file.read(file),
                    rules = defaultRules,
                    key,
                    reg,
                    result;

                if (options.ignore) {
                    for (key in options.ignore) {
                        if (options.ignore.hasOwnProperty(key)) {
                            reg = new RegExp(key + '.*?' + options.ignore[key], 'ig');
                            text = text.replace(reg, 'x');
                        }
                    }
                }

                if (options.htmlhintrc) {
                    rules = grunt.file.readJSON(options.htmlhintrc);
                } else if (options.rules) {
                    if (extendRules) {
                        rules = _.extend(defaultRules, options.rules);
                    } else {
                        rules = options.rules;
                    }
                }

                result = HTMLHint.verify(text, rules);
                if ((result.length > 0) && (_.includes(outputTypes, 'default') || _.includes(outputTypes, 'console'))) {
                    hasError = true;
                    grunt.log.errorlns('HtmlHintPlus: ' + result.length + ' warnings in ' + file + '...');
                    result.forEach(function (msg) {
                        grunt.log.writeln("[".red + ("L" + msg.line).yellow + ":".red + ("C" + msg.col).yellow + "]".red + ' ' + msg.message.yellow);
                    });
                } else {
                    if (options.newer) {
                        fc.addFile(file).update(file);
                    }
                    grunt.verbose.ok('HtmlHintPlus: ' + file + ' hint well...');
                }

                // update the reduced results collection with this file's info
                if (_.includes(outputTypes, 'checkstyle') || _.includes(outputTypes, 'json') || _.includes(outputTypes, 'text')) {
                    reducedResults.push({
                        filename: file,
                        messages: _.map(result, function (message) {
                            return {
                                line: message.line,
                                column: message.col,
                                severity: message.type,
                                message: message.message,
                                rule: message.rule.id + ': ' + message.rule.description
                            };
                        })
                    });
                }
            });
        });

        fc.save();

        // Make sure that the htmlhint directory exists
        if (_.includes(outputTypes, 'checkstyle') || _.includes(outputTypes, 'json')) {
            try {
                fs.accessSync(process.cwd() + '/htmlhint-report');
            } catch (e) {
                fs.mkdirSync(process.cwd() + '/htmlhint-report');
            }
        }

        // Write the checkstyle file
        if (_.includes(outputTypes, 'checkstyle')) {
            try {
                fs.unlinkSync(process.cwd() + '/htmlhint-report/htmlhint-checkstyle-report.xml');
            } catch (e) { }
            try {
                fs.writeFileSync(process.cwd() + '/htmlhint-report/htmlhint-checkstyle-report.xml', checkstyleFormatter(reducedResults));
            } catch (e) {
                grunt.log.writeln("Unable to write ".red + "htmlhint-checkstyle-report.xml".white.bold);
            }
        }

        if (_.includes(outputTypes, 'json')) {
            try {
                fs.unlinkSync(process.cwd() + '/htmlhint-report/htmlhint-checkstyle-report.json');
            } catch (e) { }
            try {
                fs.writeFileSync(process.cwd() + '/htmlhint-report/htmlhint-checkstyle-report.json', JSON.stringify(reducedResults));
            } catch (e) {
                grunt.log.writeln("Unable to write ".red + "htmlhint-checkstyle-report.json".white.bold);
            }
        }

        if (_.includes(outputTypes, 'text')) {
            try {
                fs.unlinkSync(process.cwd() + '/htmlhint-report/htmlhint-checkstyle-report.txt');
            } catch (e) { }
            try {
                var textResults = [];
                for (var i = 0, len = reducedResults.length; i < len; i++) {
                    textResults.push('HtmlHintPlus: ' + reducedResults[i].messages.length + ' warnings in ' + reducedResults[i].filename + '...');
                    textResults.push(_.map(reducedResults[i].messages, function (message) {
                        return "[" + ("L" + message.line) + ":" + ("C" + message.column) + "]" + ' ' + message.message;
                    }));
                }
                textResults = _.flatten(textResults);
                fs.writeFileSync(process.cwd() + '/htmlhint-report/htmlhint-checkstyle-report.txt', textResults.join('\n'));
            } catch (e) {
                grunt.log.writeln("Unable to write ".red + "htmlhint-checkstyle-report.txt".white.bold);
            }
        }

        if (options.force && hasError) {
            grunt.fail.fatal('Can\'t pass htmlhint. Set \'force\' option to continue.');
        }
    });
};