├── .gitignore ├── .jscsrc ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── UNLICENSE ├── bin └── css-validator ├── docs └── examples.js ├── lib ├── cli.js ├── css-validator.js ├── validation-stream.js └── xml-parser.js ├── package.json └── test ├── cli_test.js ├── css-validator_test.js ├── test-files ├── fake-jigsaw │ ├── GET_%2Fcss-validator%2Fvalidator%3Fo_319e22637d6dc24aa73a46c650902961.json │ ├── GET_%2Fcss-validator%2Fvalidator%3Fo_866e5724035789665269ec61dc1641a4.json │ ├── POST_%2Fcss-validator%2Fvalidator_526485bb7d0707a4b57a91373385e04e.json │ ├── POST_%2Fcss-validator%2Fvalidator_70e94cf96374050f7556586668bf6184.json │ ├── POST_%2Fcss-validator%2Fvalidator_aa857c07950aabae1aa1f97d95880a5e.json │ ├── POST_%2Fcss-validator%2Fvalidator_b01b6dda79fbb8a05a4313bd36d18166.json │ ├── POST_%2Fcss-validator%2Fvalidator_bac997c6520131945d11659def6876b3.json │ ├── POST_%2Fcss-validator%2Fvalidator_d0fc59552c514a6cfde2b979f258009e.json │ ├── POST_%2Fcss-validator%2Fvalidator_d56efd3da2bc6ea5b0c8346843f619dc.json │ └── POST_%2Fcss-validator%2Fvalidator_e635bbc34c4e4b6f66663de649b058e5.json ├── invalid.css ├── invalid.xml └── valid.css └── utils └── fake-jigsaw.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "requireCurlyBraces": [ 3 | "if", 4 | "else", 5 | "for", 6 | "while", 7 | "do", 8 | "try", 9 | "catch", 10 | "finally", 11 | "with" 12 | ], 13 | "requireSpaceAfterKeywords": true, 14 | "requireSpaceBeforeBlockStatements": true, 15 | "requireSpacesInConditionalExpression": true, 16 | "requireSpacesInFunctionExpression": { 17 | "beforeOpeningRoundBrace": true, 18 | "beforeOpeningCurlyBrace": true 19 | }, 20 | "requireSpacesInFunctionDeclaration": { 21 | "beforeOpeningCurlyBrace": true 22 | }, 23 | "disallowSpacesInFunctionDeclaration": { 24 | "beforeOpeningRoundBrace": true 25 | }, 26 | "disallowSpacesInCallExpression": true, 27 | "disallowMultipleVarDecl": true, 28 | "requireBlocksOnNewline": 1, 29 | "disallowPaddingNewlinesInBlocks": true, 30 | "disallowSpacesInsideObjectBrackets": "all", 31 | "disallowSpacesInsideArrayBrackets": "all", 32 | "disallowSpacesInsideParentheses": true, 33 | "disallowQuotedKeysInObjects": "allButReserved", 34 | "disallowSpaceAfterObjectKeys": true, 35 | "requireSpaceBeforeObjectValues": true, 36 | "requireCommaBeforeLineBreak": true, 37 | "requireOperatorBeforeLineBreak": true, 38 | "disallowSpaceAfterPrefixUnaryOperators": true, 39 | "disallowSpaceBeforePostfixUnaryOperators": true, 40 | "requireSpaceBeforeBinaryOperators": true, 41 | "requireSpaceAfterBinaryOperators": true, 42 | "requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties", 43 | "disallowKeywords": [ 44 | "with" 45 | ], 46 | "disallowMultipleLineStrings": true, 47 | "disallowMultipleLineBreaks": true, 48 | "disallowMixedSpacesAndTabs": true, 49 | "disallowTrailingWhitespace": true, 50 | "disallowTrailingComma": true, 51 | "disallowKeywordsOnNewLine": [ 52 | "else", 53 | "catch", 54 | "finally" 55 | ], 56 | "requireLineFeedAtFileEnd": true, 57 | "maximumLineLength": { 58 | "value": 120, 59 | "allowUrlComments": true 60 | }, 61 | "requireDotNotation": true, 62 | "disallowYodaConditions": true, 63 | "requireSpaceAfterLineComment": true, 64 | "disallowNewlineBeforeBlockStatements": true, 65 | "validateLineBreaks": "LF", 66 | "validateQuoteMarks": { 67 | "mark": "'", 68 | "escape": true 69 | }, 70 | "validateIndentation": 2, 71 | "validateParameterSeparator": ", ", 72 | "safeContextKeyword": [ 73 | "that" 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "eqeqeq": true, 3 | "freeze": true, 4 | "immed": true, 5 | "latedef": true, 6 | "nonbsp": true, 7 | "undef": true, 8 | "strict": false, 9 | "node": true, 10 | "sub": false, 11 | "globals": { 12 | "exports": true, 13 | "describe": true, 14 | "before": true, 15 | "beforeEach": true, 16 | "after": true, 17 | "afterEach": true, 18 | "it": true 19 | }, 20 | "curly": true, 21 | "indent": 2, 22 | "newcap": true, 23 | "noarg": true, 24 | "quotmark": "single", 25 | "unused": "vars", 26 | "maxparams": 4, 27 | "maxdepth": 5 28 | } 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "14" 5 | - "12" 6 | 7 | before_install: 8 | - curl --location http://rawgit.com/twolfson/fix-travis-ci/master/lib/install.sh | bash -s 9 | 10 | notifications: 11 | email: 12 | recipients: 13 | - todd@twolfson.com 14 | on_success: change 15 | on_failure: change 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # css-validator changelog 2 | 0.11.0 - Added HTTPS support via @allthegoodusernamesaregone in #19 3 | 4 | 0.10.0 - Upgraded dependencies, fixed delay, and updated fixtures via @antongolub in #18 5 | 6 | 0.9.0 - Added more CLI validation options via @antongolub in #17 7 | 8 | 0.8.1 - Upgraded to express@3.21.2 to fix GitHub vulnerability warning 9 | 10 | 0.8.0 - Added empty CSS support via @niedzielski in #7 and fixed URI support 11 | 12 | 0.7.0 - Added CLI via @niedzielski in #5 13 | 14 | 0.6.0 - Moved from Grunt linting to `twolfson-style` 15 | 16 | 0.5.3 - Repaired mistakes in README. Fixes #3 and #4 17 | 18 | 0.5.2 - Added `foundry` for consitent releases 19 | 20 | 0.5.1 - Moved to `eight-track` for HTTP fixtures over `nock` 21 | 22 | 0.5.0 - Broke up code further, moved XmlParser and ValidationStream to Duplex, and emitting result as `data` event 23 | 24 | 0.4.0 - Implemented CssValidator as a Writable stream 25 | 26 | 0.3.0 - Moved from GET to POST for requests. Fixes #1 27 | 28 | 0.2.0 - Added ability to get event emitter 29 | 30 | 0.1.1 - Added missing error callback 31 | 32 | 0.1.0 - Initial release 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # css-validator [![Build status](https://travis-ci.org/twolfson/css-validator.png?branch=master)](https://travis-ci.org/twolfson/css-validator) 2 | 3 | Validate CSS via [W3C's service][jigsaw] 4 | 5 | [jigsaw]: http://jigsaw.w3.org/css-validator/ 6 | 7 | This was created to validate CSS inside of the [json2css][] test suite. 8 | 9 | [json2css]: https://github.com/twolfson/json2css 10 | 11 | ## Getting Started 12 | Install the module with: `npm install css-validator` 13 | 14 | ```js 15 | var validateCss = require('css-validator'); 16 | var assert = require('assert'); 17 | validateCss({text: 'a { color: blue; }'}, function (err, data) { 18 | assert.strictEqual(data.validity, true); 19 | assert.deepEqual(data.errors, []); 20 | assert.deepEqual(data.warnings, []); 21 | }); 22 | ``` 23 | 24 | ## Donations 25 | Support this project and [others by twolfson][projects] via [donations][support-me] 26 | 27 | [projects]: http://twolfson.com/projects 28 | [support-me]: http://twolfson.com/support-me 29 | 30 | ## Documentation 31 | `css-validator` returns a single function as its `module.exports` 32 | 33 | ### `validateCss(options, cb)` 34 | Validate CSS against [W3C's Jigsaw validation service][jigsaw] 35 | 36 | - options `String|Object` - If `options` is a `String`, it will be treated as `options.text` 37 | - w3cUrl `String` - URL to validate against. Default is http://jigsaw.w3.org/css-validator/validator 38 | - The following options from the validator itself 39 | - Reference: http://jigsaw.w3.org/css-validator/manual.html#api 40 | - uri `null|String` - URL of document to validate. CSS and HTML documents are allowed. Supports http and https protocols. 41 | - text `null|String` - CSS to validate 42 | - usermedium `String` - Medium where the CSS will be used (e.g. `all`, `print`, `screen`) 43 | - Service's default value: `all` 44 | - profile `String` - CSS profile to use for validation (e.g. `css3svg`, `css21`, `svg`) 45 | - Service's default value: `css3svg` 46 | - lang `String` - Language to use in response (e.g. `en`, `bg`, `de`) 47 | - Service's default value: `en` 48 | - warning `Number|String` - Warning level to set. Default is `2` 49 | - Service's default value: `2` 50 | - If set to `no`, no warnings will be returned 51 | - If set to `0`, less warnings will be returned 52 | - If set to `1` or `2`, more warnings will be returned 53 | - vextwarning `String|Boolean` - Allow vendor extensions to just show up as warnings 54 | - Possible values: `false`, `true` 55 | - Service's default value: `false` 56 | - cb `null|Function` - Error first callback with `function (err, data) {}` signature 57 | - err `null|Error` - If there was a connetivity error, this will be it 58 | - data `null|Object` - Container for response from [jigsaw][] 59 | - validity `Boolean` - If there were no errors, this will be `true`. Otherwise, it is `false`. 60 | - errors `Object[]` - Array of errors 61 | - These are dynamically parsed and not guaranteed to exist. The service only guarantees `line`, `level`, and `message`. 62 | - Reference: http://jigsaw.w3.org/css-validator/api.html#soap12message 63 | - line `Number` - Line where error occurred 64 | - errortype `String` 65 | - context `String` 66 | - errorsubtype `String` 67 | - skippedstring `String` - Content where error occurred 68 | - message `String` - Human readable information about the error and why it occurred 69 | - warnings `Object[]` - Array of warnings 70 | - line `Number` - Line where error occurred 71 | - level `Number` - Intensity of the warning. See `options.warning` for more info 72 | - message `String` - Human readable information about the warning and why it occurred 73 | 74 | If `cb` is not provided, a [`DuplexStream`][] will be returned to you. 75 | 76 | If you have not provided `options.uri` or `options.text`, you can `.write` + `.end` OR `.pipe` to the stream CSS to validate. 77 | 78 | Additionally, you can use `.read` and `.pipe` to get the `data` returned by `cb`. 79 | 80 | The stream will emit the following events: 81 | 82 | - error `Error` - Error occurring during connection or parsing of response 83 | - data `Object` - Same as `data` sent to `cb`. Emitted once. 84 | - end - Emitted when we have finished parsing the input and outputting events 85 | - validity `Boolean` - Event for `data.validity` with `data.validity` as its data 86 | - validation-error `Object` - Event for a new `data.errors` object with the error as its argument 87 | - validation-warning `Object` - Event for a new `data.warnings` object with the warning as its argument 88 | 89 | [`DuplexStream`]: https://github.com/isaacs/readable-stream#class-streamduplex 90 | 91 | ### CLI 92 | `css-validator` offers a command line interface for validation: 93 | 94 | ``` 95 | $ css-validator --help 96 | 97 | Usage: css-validator [options] 98 | 99 | Options: 100 | 101 | -h, --help output usage information 102 | -V, --version output the version number 103 | --w3c-url URL to validate against. Default is http://jigsaw.w3.org/css-validator/validator 104 | --delay Delay between validation requests to avoid service blacklisting, defaults to 100ms 105 | --concurrency Amount of requests to run in parallel, defaults to 1 106 | --usermedium Medium where the CSS will be used (e.g. `all` (service default), `print`, `screen`) 107 | --profile CSS profile to use for validation (e.g. `css3svg` (service default), `css21`, `svg`) 108 | --lang Language to use in response (e.g. `en` (service default), `bg`, `de`) 109 | --warning Warning level to set (e.g. `0`, `1`, `2` (service default), `no`) 110 | --vextwarning Allow vendor extensions to just show up as warnings. Possible values are: `true`, `false` (service default) 111 | ``` 112 | 113 | ## Examples 114 | ```js 115 | var cssValidate = require('css-validator'); 116 | var css = [ 117 | "body {", 118 | " background: url(ab'cd');", 119 | " -moz-box-sizing: content-box;", 120 | "}", 121 | ].join('\n'); 122 | 123 | cssValidate(css, function (err, data) { 124 | console.log(data); 125 | /* 126 | { validity: false, 127 | errors: 128 | [ { line: '2', 129 | errortype: 'parse-error', 130 | context: ' body ', 131 | errorsubtype: '\n exp\n ', 132 | skippedstring: '\n url(ab \'cd\')\n ', 133 | message: '\n \n Value Error : background (nullcolors.html#propdef-background)\n \n url(ab \'cd\') is not a background-color value : \n ', 134 | error: '\n ' } ], 135 | warnings: 136 | [ { line: '3', 137 | level: '0', 138 | message: 'Property -moz-box-sizing is an unknown vendor extension', 139 | warning: '\n ' } ] } 140 | */ 141 | }); 142 | ``` 143 | 144 | ## Contributing 145 | In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint via `npm run lint` and test via `npm test`. 146 | 147 | ## Unlicense 148 | As of Nov 27 2013, Todd Wolfson has released this repository and its contents to the public domain. 149 | 150 | It has been released under the [UNLICENSE][]. 151 | 152 | [UNLICENSE]: UNLICENSE 153 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /bin/css-validator: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // Load in our dependencies 3 | var cli = require('../lib/cli'); 4 | 5 | // Parse our arguments 6 | cli.parse(process.argv); 7 | -------------------------------------------------------------------------------- /docs/examples.js: -------------------------------------------------------------------------------- 1 | var cssValidate = require('../'); 2 | var css = [ 3 | "body {", 4 | " background: url(ab'cd');", 5 | " -moz-box-sizing: content-box;", 6 | "}", 7 | ].join('\n'); 8 | 9 | cssValidate(css, function (err, data) { 10 | console.log(data); 11 | }); -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // Load in our dependencies 3 | var async = require('async'); 4 | var fs = require('fs'); 5 | var Command = require('commander').Command; 6 | var ValidationStream = require('../lib/validation-stream'); 7 | var package = require('../package.json'); 8 | var validateCss = require('../lib/css-validator'); 9 | 10 | // Define our constants 11 | var DEFAULT_SLEEP_MS = 100; 12 | var DEFAULT_CONCURRENCY = 1; 13 | var STATUS_OK = 0; 14 | var STATUS_WARN = 1; 15 | var STATUS_ERR = 2; 16 | 17 | // Define our program 18 | exports._parse = function (argv, _console, callback) { 19 | // Define our program 20 | // https://github.com/tj/commander.js/blob/v2.9.0/index.js#L17 21 | var program = new Command(); 22 | program 23 | .version(package.version) 24 | .usage('[options] ') 25 | .option('--w3c-url ', 'URL to validate against. Default is ' + ValidationStream.w3cUrl) 26 | // Waiting between calls is recommend: https://jigsaw.w3.org/css-validator/about.html#api 27 | .option('--delay ', 28 | 'Delay between validation requests to avoid service blacklisting, defaults to ' + DEFAULT_SLEEP_MS + 'ms', 29 | DEFAULT_SLEEP_MS) 30 | .option('--concurrency ', 31 | 'Amount of requests to run in parallel, defaults to ' + DEFAULT_CONCURRENCY, DEFAULT_CONCURRENCY) 32 | .option('--usermedium ', 33 | 'Medium where the CSS will be used (e.g. `all` (service default), `print`, `screen`)') 34 | .option('--profile ', 35 | 'CSS profile to use for validation (e.g. `css3svg` (service default), `css21`, `svg`)') 36 | .option('--lang ', 'Language to use in response (e.g. `en` (service default), `bg`, `de`)') 37 | .option('--warning ', 'Warning level to set (e.g. `0`, `1`, `2` (service default), `no`)') 38 | .option('--vextwarning ', 39 | 'Allow vendor extensions to just show up as warnings. ' + 40 | 'Possible values are: `true`, `false` (service default)'); 41 | 42 | // Process our arguments 43 | program.parse(argv); 44 | 45 | // Get program opts 46 | var opts = program.opts(); 47 | 48 | // Assume we are OK by default 49 | var status = STATUS_OK; 50 | 51 | // Load our files in parallel (eliminates existence errors upfront) 52 | var filepaths = program.args; 53 | async.map(filepaths, function loadFilepath (filepath, cb) { 54 | fs.readFile(filepath, 'utf8', cb); 55 | }, function handleLoadFilepaths (err, filepathContentArr) { 56 | // If there was an error, callback with it 57 | if (err) { 58 | return callback(err); 59 | } 60 | 61 | // Otherwise, perform our validation 62 | async.eachOfLimit(filepathContentArr, opts.concurrency, 63 | function handleFilepathContent (filepathContent, i, cb) { 64 | var filepath = filepaths[i]; 65 | var options = { 66 | text: filepathContent, 67 | w3cUrl: opts.w3cUrl, 68 | usermedium: opts.usermedium, 69 | profile: opts.profile, 70 | lang: opts.lang, 71 | warning: opts.warning, 72 | vextwarning: opts.vextwarning 73 | }; 74 | 75 | validateCss(options, function handleValidateCss (err, data) { 76 | // If we had an error, then callback with it 77 | if (err) { 78 | return cb(err); 79 | } 80 | 81 | // Output our errors and adjust our status 82 | if (data.warnings.length) { 83 | data.warnings.forEach(function handleWarning (warningObj) { 84 | // index.css:3: 85 | // Property -moz-box-sizing is an unknown vendor extension 86 | _console.error(filepath + ':' + warningObj.line + ':\n ' + warningObj.message); 87 | }); 88 | status = Math.max(STATUS_WARN, status); 89 | } 90 | if (data.errors.length) { 91 | data.errors.forEach(function handleError (errorObj) { 92 | // index.css:2: 93 | // Value Error : background (nullcolors.html#propdef-background) 94 | // url(ab \'cd\') is not a background-color value 95 | var cleanedMesssge = errorObj.message 96 | .replace(/\n \n /g, '\n ') 97 | .replace(/\n\s+$/, ''); 98 | _console.error(filepath + ':' + errorObj.line + ': ' + cleanedMesssge); 99 | }); 100 | status = Math.max(STATUS_ERR, status); 101 | } 102 | 103 | // Continue 104 | setTimeout(cb, opts.delay); 105 | }); 106 | }, function handleResults (err) { 107 | // If there was an error, callback with it 108 | if (err) { 109 | return callback(err); 110 | } 111 | 112 | // Otherwise, callback with our status code 113 | callback(null, status); 114 | }); 115 | }); 116 | }; 117 | exports.parse = function (argv) { 118 | exports._parse(argv, global.console, function handleResults (err, status) { 119 | // If there was an error, throw it 120 | if (err) { 121 | throw err; 122 | } 123 | 124 | // Otherwise, exit with our status code 125 | process.exit(status); 126 | }); 127 | }; 128 | -------------------------------------------------------------------------------- /lib/css-validator.js: -------------------------------------------------------------------------------- 1 | var ValidationStream = require('./validation-stream'); 2 | 3 | // Define our CSS validator 4 | function validateCss(options, cb) { 5 | // Create an emitter 6 | var validator = new ValidationStream(options); 7 | 8 | if (cb) { 9 | // Callback with any errors 10 | validator.on('error', cb); 11 | 12 | // Save the result when emitted 13 | var result; 14 | validator.on('data', function (_result) { 15 | result = _result; 16 | }); 17 | 18 | // Callback when done 19 | validator.on('end', function () { 20 | cb(null, result); 21 | }); 22 | } else { 23 | return validator; 24 | } 25 | } 26 | 27 | // Export validateCss 28 | module.exports = validateCss; 29 | -------------------------------------------------------------------------------- /lib/validation-stream.js: -------------------------------------------------------------------------------- 1 | // Load in dependencies 2 | var http = require('http'); 3 | var https = require('https'); // jigsaw.w3.org requires https from March 2022 or perhaps earlier 4 | var querystring = require('querystring'); 5 | var url = require('url'); 6 | var util = require('util'); 7 | 8 | var FormData = require('form-data'); 9 | var extend = require('obj-extend'); 10 | var Readable = require('readable-stream').Readable; 11 | 12 | var XmlParser = require('./xml-parser'); 13 | 14 | // Define our constants 15 | var TYPE_URI = 'uri'; 16 | var TYPE_TEXT = 'text'; 17 | 18 | // We must make https:// requests with the https lib, 19 | // and http:// requests with the http lib. They error if they are given the wrong protocol. 20 | // Default to http (no 's') to maintain consistency with the pre-https versions of this file. 21 | function httpRequester(url) { 22 | return url && url.startsWith('https://') ? https : http; 23 | } 24 | 25 | // TODO: Consider this structure some more. It is good little parts but nothing is that reusable... 26 | function ValidationStream(options) { 27 | // If options is a string, upcast it to an object 28 | if (typeof options === 'string') { 29 | var css = options; 30 | options = {text: css}; 31 | } 32 | 33 | // Clone the data to prevent mutation 34 | options = extend({}, options, { 35 | objectMode: true 36 | }); 37 | this.options = options; 38 | 39 | // Inheret from Readable 40 | Readable.call(this, this.options); 41 | 42 | // Resolve our target type 43 | var type; 44 | if (this.options.hasOwnProperty('text')) { 45 | type = TYPE_TEXT; 46 | } else if (this.options.hasOwnProperty('uri')) { 47 | type = TYPE_URI; 48 | } else { 49 | throw new Error('No `uri` or `text` option was provided to css-validator'); 50 | } 51 | 52 | // If we have non-empty CSS 53 | var hasContent = type === TYPE_TEXT ? /\S/.test(this.options.text) : true; 54 | if (hasContent) { 55 | // Generate our request 56 | if (type === TYPE_TEXT) { 57 | this.generateTextRequest(this.options.text); 58 | } else { 59 | this.generateUriRequest(this.options.uri); 60 | } 61 | // Otherwise (empty CSS), emit no results 62 | } else { 63 | // DEV: The W3C service only understands nonempty CSS yet we think that should be supported 64 | // DEV: We use `process.nextTick` to avoid zalgo 65 | var that = this; 66 | process.nextTick(function handleNextTick () { 67 | that._readEmptyCss(); 68 | }); 69 | } 70 | } 71 | util.inherits(ValidationStream, Readable); 72 | extend(ValidationStream.prototype, { 73 | generateTextRequest: function (text) { 74 | // Grab the URL we are going to POST to 75 | var options = this.options; 76 | var w3cUrl = options.w3cUrl || ValidationStream.w3cUrl; 77 | 78 | // Open the request 79 | var urlParts = url.parse(w3cUrl); 80 | var form = new FormData(); 81 | urlParts.method = 'POST'; 82 | urlParts.headers = form.getHeaders(); 83 | var req = httpRequester(w3cUrl).request(urlParts); 84 | 85 | // https://jigsaw.w3.org/css-validator/api.html 86 | form.append('output', 'soap12'); 87 | Object.getOwnPropertyNames(options).forEach(function sendOption (key) { 88 | var val = options[key]; 89 | if (val && key !== 'output' && key !== 'objectMode') { 90 | form.append(key, val); 91 | } 92 | }); 93 | 94 | // Pipe the form and close the request 95 | form.pipe(req); 96 | req.end(); 97 | 98 | // Listen to our response 99 | this.listenForResponse(req); 100 | }, 101 | generateUriRequest: function (text) { 102 | // Grab the URL we are going to POST to 103 | var options = this.options; 104 | var w3cUrl = options.w3cUrl || ValidationStream.w3cUrl; 105 | 106 | // Prepare our query string data 107 | // https://jigsaw.w3.org/css-validator/api.html 108 | var qsDataObj = {output: 'soap12'}; 109 | Object.getOwnPropertyNames(options).forEach(function sendOption (key) { 110 | var val = options[key]; 111 | if (val && key !== 'output' && key !== 'objectMode') { 112 | qsDataObj[key] = val; 113 | } 114 | }); 115 | 116 | // Open the request 117 | // https://nodejs.org/api/http.html#http_http_request_options_callback 118 | var urlParts = url.parse(w3cUrl); 119 | urlParts.method = 'GET'; 120 | urlParts.path += urlParts.path.indexOf('?') === -1 ? '?' : '&'; 121 | urlParts.path += querystring.stringify(qsDataObj); 122 | var req = httpRequester(w3cUrl).request(urlParts); 123 | 124 | // Close the request 125 | req.end(); 126 | 127 | // Listen to our response 128 | this.listenForResponse(req); 129 | }, 130 | listenForResponse: function (req) { 131 | // TODO: Use a streaming form library (unfortunately, there were none) 132 | // Listen for errors from the request 133 | var that = this; 134 | req.on('error', function (err) { 135 | that.emit('error', err); 136 | }); 137 | 138 | // Listen for the response 139 | req.on('response', function (response) { 140 | // Create a validator and pipe in the response 141 | var parser = new XmlParser(); 142 | response.pipe(parser); 143 | parser.on('unpipe', function () { 144 | parser.end(); 145 | }); 146 | 147 | // Forward events back to the that 148 | XmlParser.events.forEach(function bindParserEvents (event) { 149 | parser.on(event, function forwardParserEvent (data) { 150 | that.emit(event, data); 151 | }); 152 | }); 153 | 154 | // Forward error events as well 155 | parser.on('error', function forwardParserErrors (err) { 156 | that.emit('error', err); 157 | }); 158 | 159 | // Collect validation results 160 | // TODO: This should be able to go in the XmlParser 161 | // For example: parser.aggregateData(); 162 | var validationErrors = []; 163 | var validationWarnings = []; 164 | var result = { 165 | validity: false, 166 | errors: validationErrors, 167 | warnings: validationWarnings 168 | }; 169 | parser.on('validity', function (validity) { 170 | result.validity = validity; 171 | }); 172 | parser.on('validation-error', function (err) { 173 | validationErrors.push(err); 174 | }); 175 | parser.on('validation-warning', function (warning) { 176 | validationWarnings.push(warning); 177 | }); 178 | 179 | // When we can, immediately force a read 180 | parser.on('readable', function () { 181 | parser.read(); 182 | 183 | // When the parser is complete 184 | parser.on('end', function () { 185 | // Emit the result and EOF 186 | that.push(result); 187 | that.push(null); 188 | }); 189 | }); 190 | }); 191 | }, 192 | _read: function (size) { 193 | // DEV: Do not take any action as we only emit one data event, the result 194 | }, 195 | _readEmptyCss: function () { 196 | var result = { 197 | validity: true, 198 | errors: [], 199 | warnings: [] 200 | }; 201 | this.push(result); 202 | this.push(null); 203 | } 204 | }); 205 | 206 | // Set up reference for default end point 207 | // https://jigsaw.w3.org/css-validator/manual.html#api 208 | ValidationStream.w3cUrl = 'https://jigsaw.w3.org/css-validator/validator'; 209 | 210 | module.exports = ValidationStream; 211 | -------------------------------------------------------------------------------- /lib/xml-parser.js: -------------------------------------------------------------------------------- 1 | // Load in dependencies 2 | var util = require('util'); 3 | 4 | var extend = require('obj-extend'); 5 | var Duplex = require('readable-stream').Duplex; 6 | var sax = require('sax'); 7 | 8 | // Create XmlParser 9 | function XmlParser(options) { 10 | Duplex.call(this, options); 11 | var xmlParser = sax.createStream(); 12 | this.xmlParser = xmlParser; 13 | 14 | // Set up listeners for validity OR error info 15 | var that = this; 16 | xmlParser.on('text', function (text) { 17 | var node = this._parser.tag; 18 | var nameNS = node.name.toLowerCase(); 19 | if (nameNS === 'm:validity') { 20 | that.emit('validity', text === 'true'); 21 | } else if (that.validationErr) { 22 | var name = nameNS.replace('m:', ''); 23 | that.validationErr[name] = text; 24 | } 25 | }); 26 | 27 | // Set up listeners to open/close new errors/warnings 28 | xmlParser.on('opentag', function (node) { 29 | switch (node.name.toLowerCase()) { 30 | case 'm:error': 31 | case 'm:warning': 32 | that.validationErr = {}; 33 | break; 34 | } 35 | }); 36 | xmlParser.on('closetag', function (name) { 37 | var node = this._parser.tag; 38 | switch (node.name.toLowerCase()) { 39 | case 'm:error': 40 | that.emit('validation-error', that.validationErr); 41 | that.validationErr = null; 42 | break; 43 | case 'm:warning': 44 | that.emit('validation-warning', that.validationErr); 45 | that.validationErr = null; 46 | break; 47 | case 'env:envelope': 48 | that.push(null); 49 | break; 50 | } 51 | }); 52 | 53 | // Forward errors 54 | xmlParser.on('error', function (err) { 55 | that.emit('error', err); 56 | }); 57 | } 58 | util.inherits(XmlParser, Duplex); 59 | extend(XmlParser.prototype, { 60 | _write: function (chunk, encoding, callback) { 61 | this.xmlParser.write(chunk); 62 | callback(); 63 | }, 64 | _read: function (size) { 65 | // We do not return any data. We only signal EOF. 66 | } 67 | }); 68 | XmlParser.events = [ 69 | 'validity', 70 | 'validation-error', 71 | 'validation-warning' 72 | ]; 73 | 74 | module.exports = XmlParser; 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-validator", 3 | "description": "Validate CSS via W3C's service", 4 | "version": "0.11.0", 5 | "homepage": "https://github.com/twolfson/css-validator", 6 | "author": { 7 | "name": "Todd Wolfson", 8 | "email": "todd@twolfson.com", 9 | "url": "http://twolfson.com/" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/twolfson/css-validator.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/twolfson/css-validator/issues" 17 | }, 18 | "licenses": [ 19 | { 20 | "type": "UNLICENSE", 21 | "url": "https://github.com/twolfson/css-validator/blob/master/UNLICENSE" 22 | } 23 | ], 24 | "main": "lib/css-validator", 25 | "engines": { 26 | "node": ">= 4.0.0" 27 | }, 28 | "bin": { 29 | "css-validator": "bin/css-validator" 30 | }, 31 | "scripts": { 32 | "lint": "twolfson-style lint bin/ lib/ test/", 33 | "test": "mocha && npm run lint" 34 | }, 35 | "dependencies": { 36 | "async": "~3.2.0", 37 | "commander": "~7.1.0", 38 | "form-data": "~4.0.0", 39 | "obj-extend": "~0.1.0", 40 | "readable-stream": "~3.6.0", 41 | "sax": "~1.2.4" 42 | }, 43 | "devDependencies": { 44 | "chai": "~4.3.3", 45 | "eight-track": "~2.1.0", 46 | "eight-track-normalize-multipart": "~0.2.0", 47 | "express": "~4.17.1", 48 | "foundry": "~4.4.2", 49 | "foundry-release-git": "~2.0.3", 50 | "foundry-release-npm": "~2.0.2", 51 | "jscs": "~3.0.7", 52 | "jshint": "~2.12.0", 53 | "mocha": "~8.3.1", 54 | "twolfson-style": "~1.6.1" 55 | }, 56 | "keywords": [ 57 | "css", 58 | "validate", 59 | "w3c" 60 | ], 61 | "foundry": { 62 | "releaseCommands": [ 63 | "foundry-release-git", 64 | "foundry-release-npm" 65 | ] 66 | } 67 | } -------------------------------------------------------------------------------- /test/cli_test.js: -------------------------------------------------------------------------------- 1 | // Load in our dependencies 2 | var expect = require('chai').expect; 3 | var cli = require('../lib/cli'); 4 | var FakeJigsaw = require('./utils/fake-jigsaw'); 5 | 6 | // Define test helpers 7 | function cliParse(argv, cb) { 8 | var stderr = ''; 9 | var mockConsole = { 10 | error: function (output) { 11 | stderr += output + '\n'; 12 | } 13 | }; 14 | before(function cliParseFn (done) { 15 | var that = this; 16 | cli._parse(argv, mockConsole, function handleParse (err, status) { 17 | that.err = err; 18 | that.status = status; 19 | that.stderr = stderr; 20 | done(); 21 | }); 22 | }); 23 | after(function cleanup () { 24 | delete this.err; 25 | delete this.status; 26 | delete this.stderr; 27 | }); 28 | } 29 | 30 | describe('A valid CSS file processed by our CLI', function () { 31 | FakeJigsaw.run({multipart: true}); 32 | cliParse([ 33 | 'node', 'css-validator', __dirname + '/test-files/valid.css', 34 | '--w3c-url', FakeJigsaw.w3cUrl 35 | ]); 36 | 37 | it('has no errors', function () { 38 | expect(this.err).to.equal(null); 39 | expect(this.status).to.equal(0); 40 | expect(this.stderr).to.equal(''); 41 | }); 42 | }); 43 | 44 | describe('An invalid CSS file processed by our CLI', function () { 45 | FakeJigsaw.run({multipart: true}); 46 | cliParse([ 47 | 'node', 'css-validator', __dirname + '/test-files/invalid.css', 48 | '--w3c-url', FakeJigsaw.w3cUrl 49 | ]); 50 | 51 | it('has no unexpected errors (e.g. bad URL)', function () { 52 | expect(this.err).to.equal(null); 53 | expect(this.status).to.equal(2); 54 | }); 55 | 56 | it('outputs our expected error', function () { 57 | expect(this.stderr).to.contain('invalid.css:2:'); 58 | expect(this.stderr).to.contain('background-color'); 59 | }); 60 | 61 | it('outputs our expected warning', function () { 62 | expect(this.stderr).to.contain('invalid.css:3:\n “-moz-box-sizing” is an unknown vendor extension'); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/css-validator_test.js: -------------------------------------------------------------------------------- 1 | // Load in our dependencies 2 | var fs = require('fs'); 3 | var expect = require('chai').expect; 4 | var extend = require('obj-extend'); 5 | var validateCss = require('../'); 6 | var FakeJigsaw = require('./utils/fake-jigsaw'); 7 | 8 | // Define our test helper 9 | function _runValidateCss(paramsFn) { 10 | before(function (done) { 11 | var that = this; 12 | var params = paramsFn(); 13 | validateCss(extend({ 14 | w3cUrl: 'http://localhost:1337/css-validator/validator' 15 | }, params), function (err, data) { 16 | that.err = err; 17 | that.data = data; 18 | done(); 19 | }); 20 | }); 21 | } 22 | function runValidateCssText(paramsFn) { 23 | FakeJigsaw.run({multipart: true}); 24 | _runValidateCss(paramsFn); 25 | } 26 | function runValidateCssUri(paramsFn) { 27 | FakeJigsaw.run({multipart: false}); 28 | _runValidateCss(paramsFn); 29 | } 30 | 31 | // Define our tests 32 | describe('A valid CSS file being validated', function () { 33 | runValidateCssText(function () { 34 | return { 35 | text: fs.readFileSync(__dirname + '/test-files/valid.css', 'utf8') 36 | }; 37 | }); 38 | 39 | it('has no errors', function () { 40 | expect(this.data.validity).to.equal(true); 41 | expect(this.data.errors).to.deep.equal([]); 42 | expect(this.data.warnings).to.deep.equal([]); 43 | }); 44 | }); 45 | 46 | describe('A invalid CSS file being validated', function () { 47 | runValidateCssText(function () { 48 | return { 49 | text: fs.readFileSync(__dirname + '/test-files/invalid.css', 'utf8') 50 | }; 51 | }); 52 | 53 | it('was not valid errors', function () { 54 | expect(this.data.validity).to.equal(false); 55 | }); 56 | 57 | it('has an expected error', function () { 58 | var errors = this.data.errors; 59 | expect(errors.length).to.equal(1); 60 | expect(errors[0].message).to.contain('background-color'); 61 | }); 62 | 63 | it('has an expected warning', function () { 64 | var warnings = this.data.warnings; 65 | expect(warnings.length).to.equal(1); 66 | expect(warnings[0].message).to.contain('-moz-box-sizing'); 67 | }); 68 | }); 69 | 70 | describe('A valid CSS URI being validated', function () { 71 | runValidateCssUri(function () { 72 | return { 73 | uri: 'https://gitcdn.link/repo/twolfson/css-validator/0.7.0/test/test-files/valid.css' 74 | }; 75 | }); 76 | 77 | it('has no errors', function () { 78 | expect(this.data.validity).to.equal(true); 79 | expect(this.data.errors).to.deep.equal([]); 80 | expect(this.data.warnings).to.deep.equal([]); 81 | }); 82 | }); 83 | 84 | describe('A invalid CSS URI being validated', function () { 85 | runValidateCssUri(function () { 86 | return { 87 | uri: 'https://gitcdn.link/repo/twolfson/css-validator/0.7.0/test/test-files/invalid.css' 88 | }; 89 | }); 90 | 91 | it('was not valid errors', function () { 92 | expect(this.data.validity).to.equal(false); 93 | }); 94 | 95 | it('has an expected error', function () { 96 | var errors = this.data.errors; 97 | expect(errors.length).to.equal(1); 98 | expect(errors[0].message).to.contain('background-color'); 99 | }); 100 | 101 | it('has an expected warning', function () { 102 | var warnings = this.data.warnings; 103 | expect(warnings.length).to.equal(1); 104 | expect(warnings[0].message).to.contain('-moz-box-sizing'); 105 | }); 106 | }); 107 | 108 | // Edge cases 109 | describe('An empty CSS file being validated', function () { 110 | runValidateCssText(function () { 111 | return {text: ''}; 112 | }); 113 | 114 | it('has no errors', function () { 115 | expect(this.data.validity).to.equal(true); 116 | expect(this.data.errors).to.deep.equal([]); 117 | expect(this.data.warnings).to.deep.equal([]); 118 | }); 119 | }); 120 | 121 | describe('A blank CSS file being validated', function () { 122 | runValidateCssText(function () { 123 | return {text: ' '}; 124 | }); 125 | 126 | it('has no errors', function () { 127 | expect(this.data.validity).to.equal(true); 128 | expect(this.data.errors).to.deep.equal([]); 129 | expect(this.data.warnings).to.deep.equal([]); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /test/test-files/fake-jigsaw/GET_%2Fcss-validator%2Fvalidator%3Fo_319e22637d6dc24aa73a46c650902961.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "httpVersion": "1.1", 4 | "headers": { 5 | "host": "localhost:1337", 6 | "connection": "close" 7 | }, 8 | "trailers": {}, 9 | "method": "GET", 10 | "url": "/css-validator/validator?output=soap12&w3cUrl=http%3A%2F%2Flocalhost%3A1337%2Fcss-validator%2Fvalidator&uri=https%3A%2F%2Fgitcdn.link%2Frepo%2Ftwolfson%2Fcss-validator%2F0.7.0%2Ftest%2Ftest-files%2Fvalid.css", 11 | "bodyEncoding": "utf8", 12 | "body": "" 13 | }, 14 | "response": { 15 | "httpVersion": "1.1", 16 | "headers": { 17 | "cache-control": "no-cache", 18 | "date": "Sun, 07 Mar 2021 10:21:41 GMT", 19 | "pragma": "no-cache", 20 | "transfer-encoding": "chunked", 21 | "content-language": "en", 22 | "content-type": "application/soap+xml;charset=utf-8", 23 | "server": "Jigsaw/2.3.0-beta3", 24 | "vary": "Accept-Language", 25 | "access-control-allow-origin": "*", 26 | "access-control-allow-headers": "content-type,accept-charset", 27 | "access-control-allow-methods": "GET, HEAD, POST, OPTIONS", 28 | "access-control-max-age": "600", 29 | "x-w3c-validator-errors": "0", 30 | "x-w3c-validator-status": "Valid", 31 | "connection": "close" 32 | }, 33 | "trailers": {}, 34 | "statusCode": 200, 35 | "bodyEncoding": "utf8", 36 | "body": "\n\n \n \n https://gitcdn.link/repo/twolfson/css-validator/0.7.0/test/test-files/valid.css\n http://jigsaw.w3.org/css-validator/\n css3\n 2021-03-07T10:21:41Z\n true\n \n \n 0\n \n \n \n 0\n \n \n \n \n\n\n" 37 | } 38 | } -------------------------------------------------------------------------------- /test/test-files/fake-jigsaw/GET_%2Fcss-validator%2Fvalidator%3Fo_866e5724035789665269ec61dc1641a4.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "httpVersion": "1.1", 4 | "headers": { 5 | "host": "localhost:1337", 6 | "connection": "close" 7 | }, 8 | "trailers": {}, 9 | "method": "GET", 10 | "url": "/css-validator/validator?output=soap12&w3cUrl=http%3A%2F%2Flocalhost%3A1337%2Fcss-validator%2Fvalidator&uri=https%3A%2F%2Fgitcdn.link%2Frepo%2Ftwolfson%2Fcss-validator%2F0.7.0%2Ftest%2Ftest-files%2Finvalid.css", 11 | "bodyEncoding": "utf8", 12 | "body": "" 13 | }, 14 | "response": { 15 | "httpVersion": "1.1", 16 | "headers": { 17 | "cache-control": "no-cache", 18 | "date": "Sun, 07 Mar 2021 10:21:01 GMT", 19 | "pragma": "no-cache", 20 | "transfer-encoding": "chunked", 21 | "content-language": "en", 22 | "content-type": "application/soap+xml;charset=utf-8", 23 | "server": "Jigsaw/2.3.0-beta3", 24 | "vary": "Accept-Language", 25 | "access-control-allow-origin": "*", 26 | "access-control-allow-headers": "content-type,accept-charset", 27 | "access-control-allow-methods": "GET, HEAD, POST, OPTIONS", 28 | "access-control-max-age": "600", 29 | "x-w3c-validator-errors": "1", 30 | "x-w3c-validator-status": "Invalid", 31 | "connection": "close" 32 | }, 33 | "trailers": {}, 34 | "statusCode": 200, 35 | "bodyEncoding": "utf8", 36 | "body": "\n\n \n \n https://gitcdn.link/repo/twolfson/css-validator/0.7.0/test/test-files/invalid.css\n http://jigsaw.w3.org/css-validator/\n css3\n 2021-03-07T10:21:02Z\n false\n \n \n 1\n \n \n https://gitcdn.link/cdn/twolfson/css-validator/b694a83b93fa9ffb2150dd2fbab75223a49b3bd2/test/test-files/invalid.css\n \n \n 2\n parse-error\n body \n \n exp\n \n \n url(ab 'cd')\n \n value\n \n \n \n Value Error : background (nullcolors.html#propdef-background)\n \n “url(ab 'cd')” is not a “background-color” value : \n \n \n \n \n \n \n \n 1\n \n \n https://gitcdn.link/cdn/twolfson/css-validator/b694a83b93fa9ffb2150dd2fbab75223a49b3bd2/test/test-files/invalid.css\n \n \n 3\n 0\n “-moz-box-sizing” is an unknown vendor extension\n vendor-extension\n \n \n \n \n \n \n \n\n\n" 37 | } 38 | } -------------------------------------------------------------------------------- /test/test-files/fake-jigsaw/POST_%2Fcss-validator%2Fvalidator_526485bb7d0707a4b57a91373385e04e.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "httpVersion": "1.1", 4 | "headers": { 5 | "content-type": "multipart/form-data; boundary=--------------------------823789241125776521034436", 6 | "host": "localhost:1337", 7 | "connection": "close", 8 | "transfer-encoding": "chunked" 9 | }, 10 | "trailers": {}, 11 | "method": "POST", 12 | "url": "/css-validator/validator", 13 | "bodyEncoding": "utf8", 14 | "body": "----------------------------823789241125776521034436\r\nContent-Disposition: form-data; name=\"output\"\r\n\r\nsoap12\r\n----------------------------823789241125776521034436\r\nContent-Disposition: form-data; name=\"w3cUrl\"\r\n\r\nhttp://localhost:1337/css-validator/validator\r\n----------------------------823789241125776521034436\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\nbody {\n background: url(abcd);\n}\n\r\n----------------------------823789241125776521034436--" 15 | }, 16 | "response": { 17 | "httpVersion": "1.1", 18 | "headers": { 19 | "cache-control": "no-cache", 20 | "date": "Sun, 07 Mar 2021 10:21:40 GMT", 21 | "pragma": "no-cache", 22 | "transfer-encoding": "chunked", 23 | "content-language": "en", 24 | "content-type": "application/soap+xml;charset=utf-8", 25 | "server": "Jigsaw/2.3.0-beta3", 26 | "vary": "Accept-Language", 27 | "access-control-allow-origin": "*", 28 | "access-control-allow-headers": "content-type,accept-charset", 29 | "access-control-allow-methods": "GET, HEAD, POST, OPTIONS", 30 | "access-control-max-age": "600", 31 | "x-w3c-validator-errors": "0", 32 | "x-w3c-validator-status": "Valid", 33 | "connection": "close" 34 | }, 35 | "trailers": {}, 36 | "statusCode": 200, 37 | "bodyEncoding": "utf8", 38 | "body": "\n\n \n \n TextArea\n http://jigsaw.w3.org/css-validator/\n css3\n 2021-03-07T10:21:40Z\n true\n \n \n 0\n \n \n \n 0\n \n \n \n \n\n\n" 39 | } 40 | } -------------------------------------------------------------------------------- /test/test-files/fake-jigsaw/POST_%2Fcss-validator%2Fvalidator_70e94cf96374050f7556586668bf6184.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "httpVersion": "1.1", 4 | "headers": { 5 | "content-type": "multipart/form-data; boundary=--------------------------322023314191257846612438", 6 | "host": "localhost:1337", 7 | "connection": "close", 8 | "transfer-encoding": "chunked" 9 | }, 10 | "trailers": {}, 11 | "method": "POST", 12 | "url": "/css-validator/validator", 13 | "bodyEncoding": "utf8", 14 | "body": "----------------------------322023314191257846612438\r\nContent-Disposition: form-data; name=\"output\"\r\n\r\nsoap12\r\n----------------------------322023314191257846612438\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\nbody {\n background: url(ab'cd');\n -moz-box-sizing: content-box;\n}\n\n\r\n----------------------------322023314191257846612438\r\nContent-Disposition: form-data; name=\"w3cUrl\"\r\n\r\nhttp://localhost:1337/css-validator/validator\r\n----------------------------322023314191257846612438--" 15 | }, 16 | "response": { 17 | "httpVersion": "1.1", 18 | "headers": { 19 | "cache-control": "no-cache", 20 | "date": "Sun, 07 Mar 2021 10:21:40 GMT", 21 | "pragma": "no-cache", 22 | "transfer-encoding": "chunked", 23 | "content-language": "en", 24 | "content-type": "application/soap+xml;charset=utf-8", 25 | "server": "Jigsaw/2.3.0-beta3", 26 | "vary": "Accept-Language", 27 | "access-control-allow-origin": "*", 28 | "access-control-allow-headers": "content-type,accept-charset", 29 | "access-control-allow-methods": "GET, HEAD, POST, OPTIONS", 30 | "access-control-max-age": "600", 31 | "x-w3c-validator-errors": "1", 32 | "x-w3c-validator-status": "Invalid", 33 | "connection": "close" 34 | }, 35 | "trailers": {}, 36 | "statusCode": 200, 37 | "bodyEncoding": "utf8", 38 | "body": "\n\n \n \n TextArea\n http://jigsaw.w3.org/css-validator/\n css3\n 2021-03-07T10:21:40Z\n false\n \n \n 1\n \n \n file://localhost/TextArea\n \n \n 2\n parse-error\n body \n \n exp\n \n \n url(ab 'cd')\n \n value\n \n \n \n Value Error : background (nullcolors.html#propdef-background)\n \n “url(ab 'cd')” is not a “background-color” value : \n \n \n \n \n \n \n \n 1\n \n \n file://localhost/TextArea\n \n \n 3\n 0\n “-moz-box-sizing” is an unknown vendor extension\n vendor-extension\n \n \n \n \n \n \n \n\n\n" 39 | } 40 | } -------------------------------------------------------------------------------- /test/test-files/fake-jigsaw/POST_%2Fcss-validator%2Fvalidator_aa857c07950aabae1aa1f97d95880a5e.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "httpVersion": "1.1", 4 | "headers": { 5 | "content-type": "multipart/form-data; boundary=--------------------------357882119082305388164373", 6 | "host": "localhost:1337", 7 | "connection": "close", 8 | "transfer-encoding": "chunked" 9 | }, 10 | "trailers": {}, 11 | "method": "POST", 12 | "url": "/css-validator/validator", 13 | "bodyEncoding": "utf8", 14 | "body": "----------------------------357882119082305388164373\r\nContent-Disposition: form-data; name=\"output\"\r\n\r\nsoap12\r\n----------------------------357882119082305388164373\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\nbody {\n background: url(abcd);\n}\n\r\n----------------------------357882119082305388164373\r\nContent-Disposition: form-data; name=\"w3cUrl\"\r\n\r\nhttp://localhost:1337/css-validator/validator\r\n----------------------------357882119082305388164373--" 15 | }, 16 | "response": { 17 | "httpVersion": "1.1", 18 | "headers": { 19 | "cache-control": "no-cache", 20 | "date": "Sun, 07 Mar 2021 10:21:40 GMT", 21 | "pragma": "no-cache", 22 | "transfer-encoding": "chunked", 23 | "content-language": "en", 24 | "content-type": "application/soap+xml;charset=utf-8", 25 | "server": "Jigsaw/2.3.0-beta3", 26 | "vary": "Accept-Language", 27 | "access-control-allow-origin": "*", 28 | "access-control-allow-headers": "content-type,accept-charset", 29 | "access-control-allow-methods": "GET, HEAD, POST, OPTIONS", 30 | "access-control-max-age": "600", 31 | "x-w3c-validator-errors": "0", 32 | "x-w3c-validator-status": "Valid", 33 | "connection": "close" 34 | }, 35 | "trailers": {}, 36 | "statusCode": 200, 37 | "bodyEncoding": "utf8", 38 | "body": "\n\n \n \n TextArea\n http://jigsaw.w3.org/css-validator/\n css3\n 2021-03-07T10:21:40Z\n true\n \n \n 0\n \n \n \n 0\n \n \n \n \n\n\n" 39 | } 40 | } -------------------------------------------------------------------------------- /test/test-files/fake-jigsaw/POST_%2Fcss-validator%2Fvalidator_b01b6dda79fbb8a05a4313bd36d18166.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "httpVersion": "1.1", 4 | "headers": { 5 | "content-type": "multipart/form-data; boundary=--------------------------907739183840313930238424", 6 | "host": "localhost:1337", 7 | "connection": "close", 8 | "transfer-encoding": "chunked" 9 | }, 10 | "trailers": {}, 11 | "method": "POST", 12 | "url": "/css-validator/validator", 13 | "bodyEncoding": "utf8", 14 | "body": "----------------------------907739183840313930238424\r\nContent-Disposition: form-data; name=\"output\"\r\n\r\nsoap12\r\n----------------------------907739183840313930238424\r\nContent-Disposition: form-data; name=\"w3cUrl\"\r\n\r\nhttp://localhost:1337/css-validator/validator\r\n----------------------------907739183840313930238424\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\nbody {\n background: url(ab'cd');\n -moz-box-sizing: content-box;\n}\n\n\r\n----------------------------907739183840313930238424--\r\n" 15 | }, 16 | "response": { 17 | "httpVersion": "1.1", 18 | "headers": { 19 | "cache-control": "no-cache", 20 | "date": "Sun, 07 Mar 2021 10:25:09 GMT", 21 | "pragma": "no-cache", 22 | "transfer-encoding": "chunked", 23 | "content-language": "en", 24 | "content-type": "application/soap+xml;charset=utf-8", 25 | "server": "Jigsaw/2.3.0-beta3", 26 | "vary": "Accept-Language", 27 | "access-control-allow-origin": "*", 28 | "access-control-allow-headers": "content-type,accept-charset", 29 | "access-control-allow-methods": "GET, HEAD, POST, OPTIONS", 30 | "access-control-max-age": "600", 31 | "x-w3c-validator-errors": "1", 32 | "x-w3c-validator-status": "Invalid", 33 | "connection": "close" 34 | }, 35 | "trailers": {}, 36 | "statusCode": 200, 37 | "bodyEncoding": "utf8", 38 | "body": "\n\n \n \n TextArea\n http://jigsaw.w3.org/css-validator/\n css3\n 2021-03-07T10:25:09Z\n false\n \n \n 1\n \n \n file://localhost/TextArea\n \n \n 2\n parse-error\n body \n \n exp\n \n \n url(ab 'cd')\n \n value\n \n \n \n Value Error : background (nullcolors.html#propdef-background)\n \n “url(ab 'cd')” is not a “background-color” value : \n \n \n \n \n \n \n \n 1\n \n \n file://localhost/TextArea\n \n \n 3\n 0\n “-moz-box-sizing” is an unknown vendor extension\n vendor-extension\n \n \n \n \n \n \n \n\n\n" 39 | } 40 | } -------------------------------------------------------------------------------- /test/test-files/fake-jigsaw/POST_%2Fcss-validator%2Fvalidator_bac997c6520131945d11659def6876b3.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "httpVersion": "1.1", 4 | "headers": { 5 | "content-type": "multipart/form-data; boundary=--------------------------540950892174155717799911", 6 | "host": "localhost:1337", 7 | "connection": "close", 8 | "transfer-encoding": "chunked" 9 | }, 10 | "trailers": {}, 11 | "method": "POST", 12 | "url": "/css-validator/validator", 13 | "bodyEncoding": "utf8", 14 | "body": "----------------------------540950892174155717799911\r\nContent-Disposition: form-data; name=\"output\"\r\n\r\nsoap12\r\n----------------------------540950892174155717799911\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\nbody {\n background: url(ab'cd');\n -moz-box-sizing: content-box;\n}\n\n\r\n----------------------------540950892174155717799911\r\nContent-Disposition: form-data; name=\"w3cUrl\"\r\n\r\nhttp://localhost:1337/css-validator/validator\r\n----------------------------540950892174155717799911--\r\n" 15 | }, 16 | "response": { 17 | "httpVersion": "1.1", 18 | "headers": { 19 | "cache-control": "no-cache", 20 | "date": "Sun, 07 Mar 2021 10:25:09 GMT", 21 | "pragma": "no-cache", 22 | "transfer-encoding": "chunked", 23 | "content-language": "en", 24 | "content-type": "application/soap+xml;charset=utf-8", 25 | "server": "Jigsaw/2.3.0-beta3", 26 | "vary": "Accept-Language", 27 | "access-control-allow-origin": "*", 28 | "access-control-allow-headers": "content-type,accept-charset", 29 | "access-control-allow-methods": "GET, HEAD, POST, OPTIONS", 30 | "access-control-max-age": "600", 31 | "x-w3c-validator-errors": "1", 32 | "x-w3c-validator-status": "Invalid", 33 | "connection": "close" 34 | }, 35 | "trailers": {}, 36 | "statusCode": 200, 37 | "bodyEncoding": "utf8", 38 | "body": "\n\n \n \n TextArea\n http://jigsaw.w3.org/css-validator/\n css3\n 2021-03-07T10:25:09Z\n false\n \n \n 1\n \n \n file://localhost/TextArea\n \n \n 2\n parse-error\n body \n \n exp\n \n \n url(ab 'cd')\n \n value\n \n \n \n Value Error : background (nullcolors.html#propdef-background)\n \n “url(ab 'cd')” is not a “background-color” value : \n \n \n \n \n \n \n \n 1\n \n \n file://localhost/TextArea\n \n \n 3\n 0\n “-moz-box-sizing” is an unknown vendor extension\n vendor-extension\n \n \n \n \n \n \n \n\n\n" 39 | } 40 | } -------------------------------------------------------------------------------- /test/test-files/fake-jigsaw/POST_%2Fcss-validator%2Fvalidator_d0fc59552c514a6cfde2b979f258009e.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "httpVersion": "1.1", 4 | "headers": { 5 | "content-type": "multipart/form-data; boundary=--------------------------461283002974093733751980", 6 | "host": "localhost:1337", 7 | "connection": "close", 8 | "transfer-encoding": "chunked" 9 | }, 10 | "trailers": {}, 11 | "method": "POST", 12 | "url": "/css-validator/validator", 13 | "bodyEncoding": "utf8", 14 | "body": "----------------------------461283002974093733751980\r\nContent-Disposition: form-data; name=\"output\"\r\n\r\nsoap12\r\n----------------------------461283002974093733751980\r\nContent-Disposition: form-data; name=\"w3cUrl\"\r\n\r\nhttp://localhost:1337/css-validator/validator\r\n----------------------------461283002974093733751980\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\nbody {\n background: url(abcd);\n}\n\r\n----------------------------461283002974093733751980--\r\n" 15 | }, 16 | "response": { 17 | "httpVersion": "1.1", 18 | "headers": { 19 | "cache-control": "no-cache", 20 | "date": "Sun, 07 Mar 2021 10:25:09 GMT", 21 | "pragma": "no-cache", 22 | "transfer-encoding": "chunked", 23 | "content-language": "en", 24 | "content-type": "application/soap+xml;charset=utf-8", 25 | "server": "Jigsaw/2.3.0-beta3", 26 | "vary": "Accept-Language", 27 | "access-control-allow-origin": "*", 28 | "access-control-allow-headers": "content-type,accept-charset", 29 | "access-control-allow-methods": "GET, HEAD, POST, OPTIONS", 30 | "access-control-max-age": "600", 31 | "x-w3c-validator-errors": "0", 32 | "x-w3c-validator-status": "Valid", 33 | "connection": "close" 34 | }, 35 | "trailers": {}, 36 | "statusCode": 200, 37 | "bodyEncoding": "utf8", 38 | "body": "\n\n \n \n TextArea\n http://jigsaw.w3.org/css-validator/\n css3\n 2021-03-07T10:25:09Z\n true\n \n \n 0\n \n \n \n 0\n \n \n \n \n\n\n" 39 | } 40 | } -------------------------------------------------------------------------------- /test/test-files/fake-jigsaw/POST_%2Fcss-validator%2Fvalidator_d56efd3da2bc6ea5b0c8346843f619dc.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "httpVersion": "1.1", 4 | "headers": { 5 | "content-type": "multipart/form-data; boundary=--------------------------490158434652554678086072", 6 | "host": "localhost:1337", 7 | "connection": "close", 8 | "transfer-encoding": "chunked" 9 | }, 10 | "trailers": {}, 11 | "method": "POST", 12 | "url": "/css-validator/validator", 13 | "bodyEncoding": "utf8", 14 | "body": "----------------------------490158434652554678086072\r\nContent-Disposition: form-data; name=\"output\"\r\n\r\nsoap12\r\n----------------------------490158434652554678086072\r\nContent-Disposition: form-data; name=\"w3cUrl\"\r\n\r\nhttp://localhost:1337/css-validator/validator\r\n----------------------------490158434652554678086072\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\nbody {\n background: url(ab'cd');\n -moz-box-sizing: content-box;\n}\n\n\r\n----------------------------490158434652554678086072--" 15 | }, 16 | "response": { 17 | "httpVersion": "1.1", 18 | "headers": { 19 | "cache-control": "no-cache", 20 | "date": "Sun, 07 Mar 2021 10:21:41 GMT", 21 | "pragma": "no-cache", 22 | "transfer-encoding": "chunked", 23 | "content-language": "en", 24 | "content-type": "application/soap+xml;charset=utf-8", 25 | "server": "Jigsaw/2.3.0-beta3", 26 | "vary": "Accept-Language", 27 | "access-control-allow-origin": "*", 28 | "access-control-allow-headers": "content-type,accept-charset", 29 | "access-control-allow-methods": "GET, HEAD, POST, OPTIONS", 30 | "access-control-max-age": "600", 31 | "x-w3c-validator-errors": "1", 32 | "x-w3c-validator-status": "Invalid", 33 | "connection": "close" 34 | }, 35 | "trailers": {}, 36 | "statusCode": 200, 37 | "bodyEncoding": "utf8", 38 | "body": "\n\n \n \n TextArea\n http://jigsaw.w3.org/css-validator/\n css3\n 2021-03-07T10:21:41Z\n false\n \n \n 1\n \n \n file://localhost/TextArea\n \n \n 2\n parse-error\n body \n \n exp\n \n \n url(ab 'cd')\n \n value\n \n \n \n Value Error : background (nullcolors.html#propdef-background)\n \n “url(ab 'cd')” is not a “background-color” value : \n \n \n \n \n \n \n \n 1\n \n \n file://localhost/TextArea\n \n \n 3\n 0\n “-moz-box-sizing” is an unknown vendor extension\n vendor-extension\n \n \n \n \n \n \n \n\n\n" 39 | } 40 | } -------------------------------------------------------------------------------- /test/test-files/fake-jigsaw/POST_%2Fcss-validator%2Fvalidator_e635bbc34c4e4b6f66663de649b058e5.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "httpVersion": "1.1", 4 | "headers": { 5 | "content-type": "multipart/form-data; boundary=--------------------------081802144532091806951155", 6 | "host": "localhost:1337", 7 | "connection": "close", 8 | "transfer-encoding": "chunked" 9 | }, 10 | "trailers": {}, 11 | "method": "POST", 12 | "url": "/css-validator/validator", 13 | "bodyEncoding": "utf8", 14 | "body": "----------------------------081802144532091806951155\r\nContent-Disposition: form-data; name=\"output\"\r\n\r\nsoap12\r\n----------------------------081802144532091806951155\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\nbody {\n background: url(abcd);\n}\n\r\n----------------------------081802144532091806951155\r\nContent-Disposition: form-data; name=\"w3cUrl\"\r\n\r\nhttp://localhost:1337/css-validator/validator\r\n----------------------------081802144532091806951155--\r\n" 15 | }, 16 | "response": { 17 | "httpVersion": "1.1", 18 | "headers": { 19 | "cache-control": "no-cache", 20 | "date": "Sun, 07 Mar 2021 10:25:08 GMT", 21 | "pragma": "no-cache", 22 | "transfer-encoding": "chunked", 23 | "content-language": "en", 24 | "content-type": "application/soap+xml;charset=utf-8", 25 | "server": "Jigsaw/2.3.0-beta3", 26 | "vary": "Accept-Language", 27 | "access-control-allow-origin": "*", 28 | "access-control-allow-headers": "content-type,accept-charset", 29 | "access-control-allow-methods": "GET, HEAD, POST, OPTIONS", 30 | "access-control-max-age": "600", 31 | "x-w3c-validator-errors": "0", 32 | "x-w3c-validator-status": "Valid", 33 | "connection": "close" 34 | }, 35 | "trailers": {}, 36 | "statusCode": 200, 37 | "bodyEncoding": "utf8", 38 | "body": "\n\n \n \n TextArea\n http://jigsaw.w3.org/css-validator/\n css3\n 2021-03-07T10:25:08Z\n true\n \n \n 0\n \n \n \n 0\n \n \n \n \n\n\n" 39 | } 40 | } -------------------------------------------------------------------------------- /test/test-files/invalid.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: url(ab'cd'); 3 | -moz-box-sizing: content-box; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /test/test-files/invalid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | file://localhost/TextArea 8 | http://jigsaw.w3.org/css-validator/ 9 | css3 10 | 2013-11-27T10:38:34Z 11 | false 12 | 13 | 14 | 1 15 | 16 | 17 | file://localhost/TextArea 18 | 19 | 20 | 2 21 | parse-error 22 | body 23 | 24 | exp 25 | 26 | 27 | url(ab 'cd') 28 | 29 | 30 | 31 | 32 | Value Error : background (nullcolors.html#propdef-background) 33 | 34 | url(ab 'cd') is not a background-color value : 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 1 43 | 44 | 45 | file://localhost/TextArea 46 | 47 | 48 | 3 49 | 0 50 | Property -moz-box-sizing is an unknown vendor extension 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /test/test-files/valid.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: url(abcd); 3 | } 4 | -------------------------------------------------------------------------------- /test/utils/fake-jigsaw.js: -------------------------------------------------------------------------------- 1 | // Load in our dependencies 2 | var express = require('express'); 3 | var eightTrack = require('eight-track'); 4 | var normalizeMultipart = require('eight-track-normalize-multipart'); 5 | 6 | // Define our helpers 7 | exports.w3cUrl = 'http://localhost:1337/css-validator/validator'; 8 | exports.run = function (options) { 9 | before(function () { 10 | this.fakeJigsaw = express().use(eightTrack({ 11 | url: 'http://jigsaw.w3.org', 12 | fixtureDir: __dirname + '/../test-files/fake-jigsaw/', 13 | normalizeFn: options.multipart ? normalizeMultipart : null 14 | })).listen(1337); 15 | }); 16 | after(function (done) { 17 | this.fakeJigsaw.close(done); 18 | delete this.fakeJigsaw; 19 | }); 20 | }; 21 | --------------------------------------------------------------------------------