├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── lib ├── getHelpText.js └── helptext.json ├── package-lock.json ├── package.json ├── renovate.json └── test ├── cli-test.js ├── data ├── invalid.html └── valid.html └── getHelpText-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea 3 | .vs 4 | .vscode 5 | 6 | # Mac OS 7 | .DS_Store 8 | 9 | # Logs 10 | logs 11 | *.log 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (http://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directory 35 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 36 | node_modules 37 | 38 | # Env 39 | .env 40 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | after_success: 5 | - npm run coveralls 6 | deploy: 7 | provider: npm 8 | email: $NPM_EMAIL 9 | api_key: $NPM_TOKEN 10 | on: 11 | tags: true 12 | branch: master -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Geir Gåsodden 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/zrrrzzt/html-validator-cli.svg?branch=master)](https://travis-ci.org/zrrrzzt/html-validator-cli) 2 | [![Coverage Status](https://coveralls.io/repos/zrrrzzt/html-validator-cli/badge.svg?branch=master&service=github)](https://coveralls.io/github/zrrrzzt/html-validator-cli?branch=master) 3 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://github.com/feross/standard) 4 | 5 | # html-validator-cli 6 | 7 | CLI for validating html using [validator.w3.org/nu](http://validator.w3.org/nu/) 8 | 9 | Requires Node >= 8.15.3 for older versions use v5.0.0 10 | 11 | Sends ```Page is valid```to ```STDOUT``` and exits with code 0 if page is valid. 12 | 13 | Sends ```Page is not valid``` to ```STDOUT``` and exits with code 1 if page is not valid. 14 | 15 | Sends ```Page not found``` to ```STDOUT``` and exits with code 1 if page is not found. 16 | 17 | ## Installation 18 | 19 | ``` 20 | $ npm i html-validator-cli -g 21 | ``` 22 | 23 | ## Usage 24 | 25 | ``` 26 | $ html-validator 27 | ``` 28 | 29 | With file 30 | 31 | ``` 32 | $ html-validator --file= 33 | ``` 34 | 35 | With data 36 | 37 | ``` 38 | $ html-validator --data=data 39 | ``` 40 | 41 | Optional pass in format for returned data. 42 | 43 | Valid options: json, html, xhtml, xml, gnu and text (default). 44 | 45 | ``` 46 | $ html-validator --format=gnu 47 | ``` 48 | 49 | Optional pass in another validator. 50 | 51 | It needs to expose the same REST interface. 52 | 53 | ``` 54 | $ html-validator --validator='http://html5.validator.nu' 55 | ``` 56 | 57 | Optional pass in strings to ignore 58 | 59 | ``` 60 | $ html-validator --ignore='Error: Stray end tag “div”.' --ignore='Error: Stray end tag “body”.' 61 | ``` 62 | Optional pass in headers 63 | 64 | ``` 65 | $ html-validator --headers='{"foo":"doo"}' 66 | ``` 67 | 68 | To get full result from validator use --verbose 69 | 70 | ``` 71 | $ html-validator --verbose 72 | ``` 73 | 74 | Optional, only get errors use --quiet 75 | 76 | ``` 77 | $ html-validator --quiet 78 | ``` 79 | 80 | Validate a local document without setting up a tunnel 81 | 82 | ``` 83 | $ html-validator --islocal 84 | ``` 85 | 86 | returns array of error messages 87 | 88 | ```JavaScript 89 | [ 90 | { 91 | "type": "error", 92 | "lastLine": 8, 93 | "lastColumn": 32, 94 | "firstColumn": 27, 95 | "message": "Stray end tag “div”.", 96 | "extract": "aaaad code

\n<", 97 | "hiliteStart": 10, 98 | "hiliteLength": 6 99 | } 100 | ] 101 | ``` 102 | 103 | ## Related 104 | 105 | - [site-validator-cli](https://github.com/p1ho/site-validator-cli) CLI for validating a whole site or multiple pages 106 | - [html-validator](https://github.com/zrrrzzt/html-validator) API for this module 107 | 108 | ## License 109 | 110 | [MIT](LICENSE) 111 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | (async () => { 3 | const fs = require('fs') 4 | const validator = require('html-validator') 5 | const getHelpText = require('./lib/getHelpText') 6 | const pkg = require('./package.json') 7 | const query = process.argv[2] 8 | const argv = require('minimist')((process.argv.slice(2))) 9 | const options = { 10 | format: 'text', 11 | ignore: argv.ignore 12 | } 13 | 14 | const isError = item => item.type === 'error' 15 | const pageNotFound = item => item.type === 'non-document-error' 16 | 17 | if (!query || process.argv.indexOf('-h') !== -1 || process.argv.indexOf('--help') !== -1) { 18 | console.log(getHelpText()) 19 | process.exit(0) 20 | } 21 | 22 | if (process.argv.indexOf('-v') !== -1 || process.argv.indexOf('--version') !== -1) { 23 | console.log(pkg.version) 24 | process.exit(0) 25 | } 26 | 27 | if (query.indexOf('http') !== -1) { 28 | options.url = argv._[0] 29 | } 30 | 31 | if (argv.format && !argv.ignore) { 32 | options.format = argv.format 33 | } 34 | 35 | if (argv.url) { 36 | options.url = argv.url 37 | } 38 | 39 | if (argv.islocal) { 40 | options.isLocal = true 41 | } 42 | 43 | if (argv.validator) { 44 | options.validator = argv.validator 45 | } 46 | 47 | if (argv.headers) { 48 | options.headers = JSON.parse(argv.headers) 49 | } 50 | 51 | if (argv.file) { 52 | options.data = fs.readFileSync(argv.file) 53 | } 54 | 55 | if (argv.data) { 56 | options.data = argv.data 57 | } 58 | 59 | try { 60 | const data = await validator(options) 61 | let msg 62 | let validationFailed = false 63 | let documentNotFound = false 64 | 65 | if (options.format === 'json') { 66 | const errors = data.messages.filter(isError) 67 | const notFound = data.messages.filter(pageNotFound) 68 | msg = JSON.stringify(data, null, 2) 69 | if (errors.length > 0 || notFound.length > 0) { 70 | validationFailed = true 71 | documentNotFound = notFound.length > 0 72 | if (argv.quiet) { 73 | msg = JSON.stringify(errors, null, 2) 74 | } 75 | } 76 | } else if (options.ignore) { 77 | msg = data 78 | if (data.includes('Error')) { 79 | validationFailed = true 80 | } 81 | if (data.includes('non-document-error')) { 82 | documentNotFound = true 83 | } 84 | } else { 85 | msg = data 86 | if (data.includes('There were errors') || data.includes('non-document-error')) { 87 | validationFailed = true 88 | documentNotFound = data.includes('non-document-error') 89 | } 90 | } 91 | if (validationFailed) { 92 | if (!argv.verbose && !argv.quiet) { 93 | console.log(documentNotFound ? 'Page not found' : 'Page is not valid') 94 | } 95 | if (argv.verbose || argv.quiet) { 96 | console.log(msg) 97 | } 98 | process.exitCode = 1 99 | } else { 100 | if (!argv.verbose && !argv.quiet) { 101 | console.log('Page is valid') 102 | } 103 | if (argv.verbose) { 104 | console.log(msg) 105 | } 106 | } 107 | } catch (error) { 108 | console.error(error) 109 | process.exitCode = 1 110 | } 111 | })() 112 | -------------------------------------------------------------------------------- /lib/getHelpText.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = () => { 4 | const help = require('./helptext.json') 5 | return help.join('\n') 6 | } 7 | -------------------------------------------------------------------------------- /lib/helptext.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Usage: ", 3 | "", 4 | "$ html-validator ", 5 | "", 6 | "or", 7 | "$ html-validator --file=", 8 | "or", 9 | "$ html-validator --data=", 10 | "", 11 | "Optional, specify format of returned data", 12 | "Valid options: json, html, xhtml, xml, gnu and text (default)", 13 | "", 14 | "$ html-validator --format=", 15 | "", 16 | "Use another validator (requires the same api)", 17 | "", 18 | "$ html-validator --validator='http://html5.validator.nu'", 19 | "", 20 | "Optional, pass in strings to ignore", 21 | "", 22 | "$ html-validator --ignore='Error: Stray end tag “div”.' --ignore='Error: Stray end tag “body”.'", 23 | "", 24 | "Get full result from validator use --verbose", 25 | "", 26 | "$ html-validator --verbose", 27 | "", 28 | "Optional, only get errors use --quiet", 29 | "", 30 | "$ html-validator --quiet" 31 | ] -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-validator-cli", 3 | "version": "7.0.1", 4 | "description": "CLI for validating html using validator.w3.org/nu", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Geir Gåsodden", 8 | "email": "geir.gasodden@pythonia.no", 9 | "url": "https://github.com/zrrrzzt" 10 | }, 11 | "main": "index.js", 12 | "bin": { 13 | "html-validator": "index.js" 14 | }, 15 | "engines": { 16 | "node": ">=10.22.0" 17 | }, 18 | "scripts": { 19 | "test": "standard && tap --reporter=spec test/*.js", 20 | "coverage": "tap test/*.js --coverage", 21 | "coveralls": "tap --cov --coverage-report=lcov test/*.js && cat coverage/lcov.info | coveralls", 22 | "standard-fix": "standard --fix", 23 | "refresh": "rm -rf node_modules && rm package-lock.json && npm install" 24 | }, 25 | "keywords": [ 26 | "html-validator", 27 | "validation", 28 | "cli" 29 | ], 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/zrrrzzt/html-validator-cli.git" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/zrrrzzt/html-validator-cli/issues" 36 | }, 37 | "homepage": "https://github.com/zrrrzzt/html-validator-cli#readme", 38 | "dependencies": { 39 | "html-validator": "5.0.1", 40 | "minimist": "1.2.5" 41 | }, 42 | "devDependencies": { 43 | "coveralls": "3.1.0", 44 | "standard": "16.0.4", 45 | "tap": "15.1.6" 46 | }, 47 | "files": [ 48 | "lib/*.{js,json}", 49 | "index.js" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>zrrrzzt/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /test/cli-test.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap') 2 | const fs = require('fs') 3 | const exec = require('child_process').execFile 4 | const getHelpText = require('../lib/getHelpText') 5 | const pkgVersion = require('../package.json').version 6 | 7 | tap.test('It returns helptext with -h flag', function helpTextWithH (test) { 8 | exec('./index.js', ['-h'], function helpTextWithH (error, stdout, stderr) { 9 | if (error) { 10 | console.error(stderr.toString()) 11 | throw error 12 | } else { 13 | test.equal(stdout.toString().trim(), getHelpText().toString().trim()) 14 | test.end() 15 | } 16 | }) 17 | }) 18 | 19 | tap.test('It returns helptext with --help flag', function helpTextWithH (test) { 20 | exec('./index.js', ['--help'], function helpTextWithH (error, stdout, stderr) { 21 | if (error) { 22 | throw error 23 | } else { 24 | test.equal(stdout.toString().trim(), getHelpText().toString().trim()) 25 | test.end() 26 | } 27 | }) 28 | }) 29 | 30 | tap.test('It returns version with -v flag', function versionWithV (test) { 31 | exec('./index.js', ['-v'], function versionWithV (error, stdout, stderr) { 32 | if (error) { 33 | throw error 34 | } else { 35 | test.equal(stdout.toString().trim(), pkgVersion) 36 | test.end() 37 | } 38 | }) 39 | }) 40 | 41 | tap.test('It returns error on error', function testError (test) { 42 | exec('./index.js', ['npmlovesyou-do-you-love-npm'], function versionWithV (error, stdout, stderr) { 43 | test.ok(error, 'Error OK') 44 | test.end() 45 | }) 46 | }) 47 | 48 | tap.test('It returns error on validation failure', function testError (test) { 49 | exec('./index.js', ['--file=test/data/invalid.html'], function versionWithV (error, stdout, stderr) { 50 | tap.equal(error.code, 1, 'It returns error.code 1') 51 | test.equal(stdout.toString().trim(), 'Page is not valid', 'Expected message to stdout') 52 | test.end() 53 | }) 54 | }) 55 | 56 | tap.test('It returns not found for non-existing pages', function testError (test) { 57 | exec('./index.js', ['https://adsf'], function versionWithV (error, stdout, stderr) { 58 | tap.equal(error.code, 1, 'It returns error.code 1') 59 | test.equal(stdout.toString().trim(), 'Page not found', 'Expected message to stdout') 60 | test.end() 61 | }) 62 | }) 63 | 64 | tap.test('It returns correct message on validation success', function testSuccess (test) { 65 | exec('./index.js', ['--file=test/data/valid.html'], (error, stdout, stderr) => { 66 | if (error) { 67 | throw error 68 | } 69 | test.equal(stdout.toString().trim(), 'Page is valid', 'Expected message to stdout') 70 | test.end() 71 | }) 72 | }) 73 | 74 | tap.test('You can supply headers', function testSuccess (test) { 75 | exec('./index.js', ['--file=test/data/valid.html', `--headers=${JSON.stringify({ foo: 'bar' })}`], (error, stdout, stderr) => { 76 | if (error) { 77 | throw error 78 | } 79 | test.ok(stdout.toString().trim(), 'Data OK') 80 | test.end() 81 | }) 82 | }) 83 | 84 | tap.test('It returns data if file supplied', function testError (test) { 85 | exec('./index.js', ['--file=test/data/valid.html'], function versionWithV (error, stdout, stderr) { 86 | if (error) { 87 | throw error 88 | } 89 | test.ok(stdout.toString().trim(), 'Data OK') 90 | test.end() 91 | }) 92 | }) 93 | 94 | tap.test('It returns data if data supplied', function testError (test) { 95 | const data = fs.readFileSync('test/data/valid.html', 'utf-8') 96 | const cmd = `--data=${data}` 97 | exec('./index.js', [cmd], (error, stdout, stderr) => { 98 | if (error) { 99 | throw error 100 | } 101 | test.ok(stdout.toString().trim(), 'Data OK') 102 | test.end() 103 | }) 104 | }) 105 | 106 | tap.test('It returns data in supplied format for success', function testError (test) { 107 | exec('./index.js', ['--file=test/data/valid.html', '--format=json', '--verbose'], function versionWithV (error, stdout, stderr) { 108 | if (error) { 109 | throw error 110 | } 111 | test.equal(JSON.parse(stdout.toString().trim()).messages.length, 0) 112 | test.end() 113 | }) 114 | }) 115 | 116 | tap.test('It returns data in supplied format for failures', function testError (test) { 117 | exec('./index.js', ['--file=test/data/invalid.html', '--format=json', '--verbose'], function versionWithV (error, stdout, stderr) { 118 | if (error) { 119 | console.error(error) 120 | } 121 | test.equal(JSON.parse(stdout.toString().trim()).messages.length, 2) 122 | test.end() 123 | }) 124 | }) 125 | 126 | tap.test('It returns no data for success if --quiet', function testSuccessQuiet (test) { 127 | exec('./index.js', ['--file=test/data/valid.html', '--format=json', '--quiet'], function successQuiet (error, stdout, stderr) { 128 | if (error) { 129 | throw error 130 | } 131 | test.equal(stdout.toString().trim().length, 0) 132 | test.end() 133 | }) 134 | }) 135 | 136 | tap.test('It errors for failures if --quiet', function testErrorQuiet (test) { 137 | exec('./index.js', ['--file=test/data/invalid.html', '--format=json', '--quiet'], function errorQuiet (error, stdout, stderr) { 138 | if (error) { 139 | console.error(error) 140 | } 141 | test.equal(JSON.parse(stdout.toString().trim()).length, 1) 142 | test.end() 143 | }) 144 | }) 145 | 146 | tap.test('You can supply url by flag', function testError (test) { 147 | exec('./index.js', ['--url=https://www.npm.com', '--verbose'], function versionWithV (error, stdout, stderr) { 148 | if (error) { 149 | console.error(error) 150 | } 151 | test.ok(stdout.toString().trim(), 'Data OK') 152 | test.end() 153 | }) 154 | }) 155 | 156 | tap.test('You can use another validator', function testError (test) { 157 | exec('./index.js', ['--file=test/data/valid.html', '--validator=https://html5.validator.nu'], function versionWithV (error, stdout, stderr) { 158 | if (error) { 159 | throw error 160 | } 161 | test.ok(stdout.toString().trim(), 'Data OK') 162 | test.end() 163 | }) 164 | }) 165 | 166 | tap.test('It supports ignore', function testError (test) { 167 | const str = 'Error: Stray end tag “div”.' 168 | exec('./index.js', ['--file=test/data/invalid.html', `--ignore=${str}`], function versionWithV (error, stdout, stderr) { 169 | if (error) { 170 | throw error 171 | } 172 | test.equal(stdout.toString().trim(), 'Page is valid', 'Expected message to stdout') 173 | test.end() 174 | }) 175 | }) 176 | -------------------------------------------------------------------------------- /test/data/invalid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Invalid 6 | 7 | 8 |

I'm baaaaaaaaaaaad code

9 | 10 | -------------------------------------------------------------------------------- /test/data/valid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 |

I'm a very good page I am

9 | 10 | -------------------------------------------------------------------------------- /test/getHelpText-test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const tap = require('tap') 4 | const getHelpText = require('../lib/getHelpText') 5 | const helpText = require('../lib/helptext.json').join('\n') 6 | 7 | tap.equal(helpText, getHelpText(), 'It returns correct helptext') 8 | --------------------------------------------------------------------------------