Skip to content

Commit b07d66d

Browse files
committed
add HTML validation
1 parent 4dc67d5 commit b07d66d

File tree

10 files changed

+1901
-264
lines changed

10 files changed

+1901
-264
lines changed

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ cssDest: dist/assets/css
9595
jsSrc: src/js/**/**.{js,vue}
9696
jsDest: dist/assets/js
9797
htmlDest: dist/[path][name].html
98+
htmllint: true,
9899
laravelMixOptions:
99100
processCssUrls: false
100101
browserSync:
@@ -108,4 +109,24 @@ browserSync:
108109
ghostMode: false
109110
logLevel: silent
110111
proxy: null
111-
```
112+
```
113+
114+
## HTML Validation
115+
116+
> Java Development Kit > v8 required.
117+
118+
```bash
119+
java -version
120+
```
121+
122+
> Start the vnu-jar server on localhost port 8888.
123+
124+
```bash
125+
java -cp node_modules/vnu-jar/build/dist/vnu.jar nu.validator.servlet.Main 8888
126+
```
127+
128+
> Now you can build/watch HTML with W3C validation.
129+
130+
```bash
131+
npm run development -- --env.run html
132+
```

htmllint/chunkify.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict';
2+
3+
module.exports = function( files, maxChars ) {
4+
var filesChunk = [];
5+
var chunk = '';
6+
7+
for ( var f = 0, len = files.length; f < len; f++ ) {
8+
if ( chunk.length + ( files[ f ].length + 1 ) > maxChars ) {
9+
filesChunk.push( chunk );
10+
chunk = '';
11+
}
12+
chunk += '"' + files[ f ] + '" ';
13+
}
14+
filesChunk.push( chunk );
15+
return filesChunk;
16+
};

htmllint/htmllint.js

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
'use strict';
2+
3+
// replace left/right quotation marks with normal quotation marks
4+
function normalizeQuotationMarks( str ) {
5+
if ( str ) {
6+
str = str.replace( /[\u201c\u201d]/g, '"' );
7+
}
8+
return str;
9+
}
10+
11+
function lint( config, files, done ) {
12+
const path = require( 'path' )
13+
const exec = require( 'child_process' ).exec
14+
var chunkify = require( './chunkify' )
15+
const async = require( 'async' )
16+
const javadetect = require( './javadetect' )
17+
const jar = require( 'vnu-jar' )
18+
19+
const maxChars = 5000
20+
21+
// increase child process buffer to accommodate large amounts of
22+
// validation output. ( default is a paltry 200k. )
23+
// http://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback
24+
const maxBuffer = 20000 * 1024
25+
26+
// parse and, if needed, normalize error messages from HttpClient to java -jar format
27+
// java -jar: one object containing messages for all files
28+
// { messages: [{ message, type, url, ... }, ...] }
29+
// HttpClient: one object per file, separated by a newline, each object containing messages for only that file
30+
// { messages: [{ message, type, ...}, ...], url }\n{ ... }
31+
function parseErrorMessages( errors ) {
32+
var parsed = JSON.parse( config.server ? '[' + errors.trim().replace( /\n/g, ',' ) + ']' : errors );
33+
var messages = parsed.messages;
34+
if ( config.server ) {
35+
// extract "messages" property from each object and set the url of each message
36+
// this results in an array of arrays instead of array of objects, which is then flattened by concatenation
37+
messages = Array.prototype.concat.apply( [], parsed.map( function( file ) {
38+
return file.messages.map( function( message ) {
39+
message.url = file.url;
40+
return message;
41+
} );
42+
} ) );
43+
}
44+
return messages;
45+
}
46+
47+
// determine proper jarfile command and arguments
48+
function cmd( java, chunk ) {
49+
var args = '';
50+
if ( config.server ) {
51+
if ( config.server.host ) {
52+
args += ' -Dnu.validator.client.host=' + config.server.host;
53+
}
54+
if ( config.server.port ) {
55+
args += ' -Dnu.validator.client.port=' + config.server.port;
56+
}
57+
args += ' -Dnu.validator.client.out=json nu.validator.client.HttpClient';
58+
} else {
59+
args += ' --format json';
60+
}
61+
if ( config.noLangDetect ) {
62+
args += ' --no-langdetect';
63+
}
64+
var invoke = ( config.server ? '-cp' : '-jar' ) + ' "' + jar + '"' + args;
65+
// command to call java, increasing the default stack size for ia32 versions of the JRE and using the default setting for x64 versions
66+
return 'java ' + ( java.arch === 'ia32' ? '-Xss512k ' : '' ) + invoke + ' ' + chunk;
67+
}
68+
69+
javadetect( function( err, java ) {
70+
if ( err ) {
71+
throw err;
72+
}
73+
74+
var javaVersion = java.version.split( '.' ).map( Number );
75+
if ( ( javaVersion[ 0 ] !== 1 && javaVersion[ 0 ] < 8 ) || ( javaVersion[ 0 ] === 1 && javaVersion[ 1 ] < 8 ) ) {
76+
throw new Error( '\nUnsupported Java version used: ' + java.version + '. Java 8 environment or up is required!' );
77+
}
78+
79+
files = files.map((file) => path.relative('.', file))
80+
async.mapSeries( chunkify( files, maxChars ), function( chunk, cb ) {
81+
82+
exec( cmd( java, chunk, config.noLangDetect ), {
83+
'maxBuffer': maxBuffer
84+
}, function( error, stdout, stderr ) {
85+
if ( error && ( error.code !== 1 || error.killed || error.signal ) ) {
86+
cb( error );
87+
return;
88+
}
89+
90+
stderr = config.server ? stdout : stderr;
91+
var result = [];
92+
if ( stderr ) {
93+
try {
94+
result = parseErrorMessages( stderr );
95+
} catch ( err ) {
96+
throw new Error( err + '\nInvalid input follows below:\n\n' + stderr );
97+
}
98+
result.forEach( function( message ) {
99+
if ( message.url ) {
100+
message.file = path.relative( '.', message.url.replace( path.sep !== '\\' ? 'file:' : 'file:/', '' ) );
101+
}
102+
if ( config.absoluteFilePathsForReporter ) {
103+
message.file = path.resolve( message.file );
104+
}
105+
} );
106+
if ( config.ignore ) {
107+
var ignore = config.ignore instanceof Array ? config.ignore : [ config.ignore ];
108+
result = result.filter( function( message ) {
109+
// iterate over the ignore rules and test the message agains each rule.
110+
// A match should return false, which causes every( ) to return false and the message to be filtered out.
111+
return ignore.every( function( currentValue ) {
112+
if ( currentValue instanceof RegExp ) {
113+
return !currentValue.test( message.message );
114+
}
115+
return normalizeQuotationMarks( currentValue ) !== normalizeQuotationMarks( message.message );
116+
} );
117+
} );
118+
}
119+
}
120+
cb( null, result );
121+
} );
122+
}, function( error, results ) {
123+
if ( error ) {
124+
done( error );
125+
return;
126+
}
127+
128+
var result = [];
129+
for ( var r = 0, len = results.length; r < len; r++ ) {
130+
result = result.concat( results[ r ] );
131+
}
132+
133+
done( null, result.filter( function( item ) {
134+
return config.errorLevels.indexOf( item.type ) !== -1;
135+
} ) );
136+
} );
137+
} );
138+
}
139+
140+
module.exports = lint

htmllint/javadetect.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict';
2+
3+
var exec = require( 'child_process' ).exec;
4+
5+
module.exports = function( callback ) {
6+
exec( 'java -version', function( error, stdout, stderr ) {
7+
if ( error ) {
8+
return callback( error );
9+
}
10+
11+
callback( null, {
12+
version: stderr.match( /(?:java|openjdk) version "(.*)"/ )[ 1 ],
13+
arch: stderr.match( /64-Bit/ ) ? 'x64' : 'ia32'
14+
} );
15+
} );
16+
};

htmllint/reporter.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
module.exports = function( results ) {
2+
var path = require( 'path' ),
3+
files = {},
4+
out = [];
5+
6+
results.forEach( function( result ) {
7+
// Register the file
8+
result.file = path.normalize( result.file );
9+
if ( !files[ result.file ] ) {
10+
files[ result.file ] = []
11+
}
12+
13+
// Add the error
14+
files[ result.file ].push( {
15+
severity: result.type,
16+
line: result.lastLine,
17+
column: result.lastColumn,
18+
message: result.message,
19+
source: 'htmllint.Validation' + ( result.type === 'error' ? 'Error' : 'Warning' )
20+
})
21+
22+
})
23+
24+
for ( var fileName in files ) {
25+
if ( files.hasOwnProperty( fileName ) ) {
26+
out.push( fileName + ' has ' + files[ fileName ].length + ' Errors\n' );
27+
for ( var i = 0, len = files[ fileName ].length; i < len; i++ ) {
28+
var issue = files[ fileName ][ i ];
29+
out.push(
30+
( i + 1 ) + ' ' +
31+
'line ' + issue.line + ', ' +
32+
'char ' + issue.column + ': ' +
33+
issue.message
34+
);
35+
}
36+
}
37+
}
38+
39+
return out.join( '\n' )
40+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
const htmllint = require( './htmllint' )
2+
const reporter = require('./reporter')
3+
const fs = require('fs')
4+
const path = require('path')
5+
const assign = require('object-assign')
6+
const Cache = require('fs-simple-cache')
7+
const cache = new Cache({
8+
cacheDirectory: path.resolve('.htmllint')
9+
})
10+
11+
module.exports = function WebpackHTMLValidate(source, map, meta) {
12+
const options = assign({
13+
noLangDetect: false,
14+
server: {
15+
host: '127.0.0.1',
16+
port: 8888,
17+
},
18+
errorLevels: ['error'],
19+
absoluteFilePathsForReporter: false,
20+
}, this.options)
21+
22+
const callback = this.async()
23+
24+
if (content = cache.get(source, false)) {
25+
if (content === source) {
26+
if (output = cache.get(source).output) {
27+
const error = new Error(output)
28+
this.emitError(error)
29+
// return callback(error, source, map, meta)
30+
// not returning a callback error to make use of caching
31+
return callback(null, source, map, meta)
32+
}
33+
}
34+
}
35+
36+
cache.put(source, source, false)
37+
38+
htmllint(options, [cache.getPath(source, false)], (error, result) => {
39+
if (error) {
40+
error = new Error(error)
41+
this.emitError(error)
42+
}
43+
else if (result.length) {
44+
const output = reporter(result)
45+
cache.put(source, { output })
46+
error = new Error(output)
47+
this.emitError(error)
48+
}
49+
// callback(error, source, map, meta)
50+
// not returning a callback error to make use of caching
51+
callback(null, source, map, meta)
52+
})
53+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const htmllint = require( './htmllint' )
2+
const reporter = require('./reporter')
3+
4+
class WebpackHTMLValidate {
5+
constructor (options = {}) {
6+
this.options = {
7+
test: /\.html$/,
8+
noLangDetect: false,
9+
server: {
10+
host: '127.0.0.1',
11+
port: 8888,
12+
},
13+
errorLevels: ['error'],
14+
absoluteFilePathsForReporter: false,
15+
}
16+
17+
this.startTime = Date.now();
18+
this.prevTimestamps = {};
19+
}
20+
apply (compiler) {
21+
compiler.plugin('emit', (compilation, callback) => {
22+
23+
var changedFiles = Object.keys(compilation.fileTimestamps).filter(function(watchfile) {
24+
return (this.prevTimestamps[watchfile] || this.startTime) < (compilation.fileTimestamps[watchfile] || Infinity);
25+
}.bind(this));
26+
27+
this.prevTimestamps = compilation.fileTimestamps;
28+
29+
if (changedFiles.length) {
30+
for (const filename in compilation.assets) {
31+
if (this.options.test.test(filename)) {
32+
htmllint(this.options, [filename], (error, result) => {
33+
if (error) {
34+
throw new Error(error)
35+
}
36+
if (result.length) {
37+
compilation.errors.push(new Error(reporter(result)))
38+
}
39+
})
40+
}
41+
}
42+
}
43+
44+
callback()
45+
})
46+
}
47+
}
48+
49+
module.exports = WebpackHTMLValidate

0 commit comments

Comments
 (0)