makestatic-validate-html
Version:
Validates HTML documents
227 lines (201 loc) • 6.64 kB
JavaScript
const when = require('when')
const spawn = require('child_process').spawn
const XML_FMT = 'xml'
const JSON_FMT = 'json'
const TEXT_FMT = 'text'
const GNU_FMT = 'gnu'
const formats = [XML_FMT, JSON_FMT, TEXT_FMT, GNU_FMT]
/**
* Validate HTML documents using the nu HTML validator jar file.
*
* Requires that java 1.8 is installed.
*
* This implementation can run in one of two modes either it will write the
* file content to `stdin` of the java process as files are processed
* which is compatible with the `validate` phase that executes before files
* are written to disc. Or it can pass all the files as arguments to the
* validator jar when `stdin` is `false` this implies the files have been
* written to disc and is compatible with the `audit` phase.
*
* Writing to `stdin` is very slow as a new JVM is launched for each file
* whilst passing all the files as arguments runs the risk of the dreaded
* *argument list too long* shell error for very large sites.
*
* The default behaviour is to use the argument list as it is much faster.
*
* Generally it is recommended that `stdin` is disabled and this plugin is
* used during the `audit` phase.
*
* @class ValidateHtml
*
* @see https://github.com/validator/validator HTML Validator
*/
class ValidateHtml {
/**
* Create a ValidateHtml plugin.
*
* If the `jar` option is given it overrides the `vnu-jar` module.
*
* When no `format` or a bad format is given the `json` format is used.
*
* Pretty printing only applies when the output format is `json`.
*
* When the `warn` option is disabled it is equivalent to the `--errors-only`
* jar option so warnings are not displayed.
*
* @constructor ValidateHtml
* @param {Object} context the processing context.
* @param {Object} options plugin options.
*
* @option {String} [format=json] validator output format.
* @option {Boolean=false} [stdin] write each file to stdin.
* @option {Boolean=true} [pretty=true] pretty print result.
* @option {Boolean=false} [bail=true] throw error on validation failure.
* @option {Boolean=false} [warn=true] control validation warnings.
* @option {Boolean=false} [detect=false] control language detection.
* @option {String} [jar] path to the validator jar file.
*/
constructor (context, options = {}) {
this.report = {}
this.options = options
this.stdin = options.stdin !== undefined ? options.stdin : false
this.pretty = options.pretty !== undefined ? options.pretty : true
this.warn = options.warn !== undefined ? options.warn : true
this.bail = options.bail !== undefined ? options.bail : true
/* istanbul ignore next: not mocking detect option */
this.detect = options.detect !== undefined ? options.detect : false
// list of file matches when not writing to stdin
this.files = []
this.cmd = 'java'
this.format = options.format || JSON_FMT
if (!~formats.indexOf(this.format)) {
this.format = JSON_FMT
}
this.jar = options.jar || require('vnu-jar')
}
/**
* Run validation on an individual HTML file when `stdin` is set otherwise
* collect the list of files to be processed afterwards.
*
* @function sources
* @member ValidateHtml
* @param {File} file the current file.
* @param {Object} context the processing context.
*/
sources (file, context) {
const log = context.log
if (this.stdin) {
const args = this.getArguments(['-'])
log.info('[validate-html] %s', file.path)
log.info('[validate-html] %s %s', this.cmd, args.join(' '))
return this.run(context, args, file)
} else {
this.files.push(file)
}
}
/**
* @private
*/
getArguments (files) {
let args = ['-jar', this.jar, '--format', this.format]
if (!this.warn) {
args.push('--errors-only')
}
/* istanbul ignore else: not mocking detect option */
if (!this.detect) {
args.push('--no-langdetect')
}
args = args.concat.apply(args, files)
return args
}
run (context, args, file) {
const log = context.log
return when.promise((resolve, reject) => {
let report = new Buffer(0)
const ps = spawn(this.cmd, args)
ps.stderr.on('data', (buf) => {
report = Buffer.concat([report, buf], report.length + buf.length)
})
ps.stdout.pipe(process.stdout)
// TODO: break content into chunks?
if (file) {
ps.stdin.write(file.content)
}
ps.once('close', (code) => {
let result
report = report.toString()
if (this.format === JSON_FMT) {
try {
result = JSON.parse(report)
if (file) {
this.report[file.path] = result
}
} catch (e) {
/* istanbul ignore next: tough to mock this error */
return when.reject(
new Error(`failed to parse validator JSON output:\n\n${report}`))
}
}
if (result && result.messages &&
result.messages.length === 0) {
if (file) {
log.info('[validate-html] %s ✓', file.path)
} else {
log.info('[validate-html] ✓')
}
} else {
try {
this.summary(context, code, report, result)
} catch (e) {
return reject(e)
}
}
resolve()
})
if (file) {
ps.stdin.end()
}
})
}
summary (context, code, report, result) {
const log = context.log
const printer = require('./lib/print')
if (this.format === JSON_FMT && this.pretty) {
printer(log, result)
} else {
if (this.format === JSON_FMT && result) {
log.info(JSON.stringify(result, undefined, 2))
} else {
log.info(report)
}
}
if (this.bail && code > 0) {
throw new Error(`[validate-html] exit code: ${code}`)
}
}
/**
* Run validation on collected files when `stdin` is `false`.
*
* @function after
* @member ValidateHtml
* @param {Object} context the processing context.
*/
after (context) {
const log = context.log
if (!this.stdin) {
let files = this.files.map((file) => {
return file.output
})
const args = this.getArguments(files)
log.info('[validate-html] %s %s', this.cmd, args.join(' '))
return this.run(context, args)
}
}
static get test () {
return /\.(html|sgr)$/
}
static get exclude () {
return [/google[a-z0-9]+\.html$/i]
}
}
module.exports = ValidateHtml