csstree-validator
Advanced tools
Comparing version 1.6.0 to 2.0.0
@@ -0,1 +1,10 @@ | ||
## 2.0.0 (November 18, 2020) | ||
- Droped support for Nodejs < 8 | ||
- Bumped `csstree` to [1.1.1](https://github.com/csstree/csstree/releases/tag/v1.1.1) | ||
- CLI exits with code `1` and outputs messages to `stderr` when errors (#12) | ||
- Added built version for browsers: `dist/csstree-validator.js` (#11) | ||
- Added at-rule validation for name, prelude and descriptor | ||
- Added `validateAtrule`, `validateAtrulePrelude`, `validateAtruleDescriptor`, `validateRule` and `validateDeclaration` methods | ||
## 1.6.0 (October 27, 2020) | ||
@@ -2,0 +11,0 @@ |
@@ -1,7 +0,19 @@ | ||
var cli = require('clap'); | ||
var reporters = require('./reporter'); | ||
var validatePath = require('./validate.js').validatePath; | ||
var validateString = require('./validate.js').validateString; | ||
const cli = require('clap'); | ||
const reporters = require('./reporter'); | ||
const { validatePath, validateString } = require('./validators'); | ||
var command = cli.create('csstree-validate', '[fileOrDir]') | ||
function printResult(result, reporter) { | ||
const output = reporter(result); | ||
if (Object.keys(result).length > 0) { | ||
console.error(output); | ||
process.exit(1); | ||
} | ||
if (output) { | ||
console.log(output); | ||
} | ||
} | ||
const command = cli.create('csstree-validate', '[fileOrDir]') | ||
.version(require('../package.json').version) | ||
@@ -15,5 +27,5 @@ .option('-r, --reporter <name>', 'Format of output: console (default), checkstyle, json, gnu', function(name) { | ||
.action(function(args) { | ||
var options = this.values; | ||
var inputPath = args[0]; | ||
var reporter = reporters[options.reporter] || reporters.console; | ||
const options = this.values; | ||
const inputPath = args[0]; | ||
const reporter = reporters[options.reporter] || reporters.console; | ||
@@ -26,12 +38,8 @@ if (process.stdin.isTTY && !inputPath) { | ||
if (!inputPath) { | ||
var buffer = []; | ||
const buffer = []; | ||
process.stdin | ||
.on('data', function(chunk) { | ||
buffer.push(chunk); | ||
}) | ||
.on('end', function() { | ||
console.log(reporter(validateString(buffer.join(''), '<stdin>'))); | ||
}); | ||
.on('data', chunk => buffer.push(chunk)) | ||
.on('end', () => printResult(validateString(buffer.join(''), '<stdin>'), reporter)); | ||
} else { | ||
console.log(reporter(validatePath(args[0]))); | ||
printResult(validatePath(args[0]), reporter); | ||
} | ||
@@ -42,5 +50,5 @@ }); | ||
run: command.run.bind(command), | ||
isCliError: function(err) { | ||
isCliError(err) { | ||
return err instanceof cli.Error; | ||
} | ||
}; |
@@ -1,9 +0,5 @@ | ||
var validators = require('./validate.js'); | ||
module.exports = { | ||
validatePathList: validators.validatePathList, | ||
validatePath: validators.validatePath, | ||
validateFile: validators.validateFile, | ||
validateString: validators.validateString, | ||
...require('./helpers.js'), | ||
...require('./validate'), | ||
reporters: require('./reporter') | ||
}; |
@@ -8,3 +8,3 @@ // <?xml version="1.0" encoding="utf-8"?> | ||
module.exports = function(data) { | ||
var output = [ | ||
const output = [ | ||
'<?xml version="1.0" encoding="utf-8"?>', | ||
@@ -15,3 +15,3 @@ '<checkstyle version="4.3">' | ||
Object.keys(data).sort().forEach(function(name) { | ||
var errors = data[name]; | ||
const errors = data[name]; | ||
@@ -18,0 +18,0 @@ output.push( |
module.exports = function(data) { | ||
var output = []; | ||
const output = []; | ||
Object.keys(data).sort().forEach(function(filename) { | ||
var errors = data[filename]; | ||
const errors = data[filename]; | ||
output.push('# ' + filename); | ||
output.push.apply(output, errors.map(function(entry) { | ||
var error = entry.error || entry; | ||
output.push.apply(output, errors.map(function(error) { | ||
if (error.name === 'SyntaxError') { | ||
@@ -16,4 +14,4 @@ return ' [ERROR] ' + error.message; | ||
return ' * ' + | ||
String(error.message) | ||
.replace(/^[^\n]+/, entry.message) | ||
String(error.details) | ||
.replace(/^[^\n]+/, error.message) | ||
.replace(/\n/g, '\n '); | ||
@@ -20,0 +18,0 @@ })); |
// "FILENAME":LINE.COLUMN: error: MESSAGE | ||
// "FILENAME":START_LINE.COLUMN-END_LINE.COLUMN: error: MESSAGE | ||
module.exports = function(data) { | ||
var output = []; | ||
const output = []; | ||
Object.keys(data).sort().forEach(function(filename) { | ||
var errors = data[filename]; | ||
const errors = data[filename]; | ||
output.push(errors.map(function(entry) { | ||
var error = entry.error || entry; | ||
var line = entry.line || -1; | ||
var column = entry.column || -1; | ||
var position = line + '.' + column; | ||
var message = entry.message || entry.error.rawMessage; | ||
var value = error.css ? ': `' + error.css + '`' : ''; | ||
var allowed = error.syntax ? '; allowed: ' + error.syntax : ''; | ||
output.push(errors.map(function(error) { | ||
const line = error.line || -1; | ||
const column = error.column || -1; | ||
const message = error.message; | ||
const value = error.css ? ': `' + error.css + '`' : ''; | ||
const allowed = error.syntax ? '; allowed: ' + error.syntax : ''; | ||
let position = line + '.' + column; | ||
if (error.loc) { | ||
position = error.loc.start.line + '.' + | ||
error.loc.start.column + '-' + | ||
position += '-' + | ||
error.loc.end.line + '.' + | ||
@@ -22,0 +20,0 @@ error.loc.end.column; |
// [{ "name": {file}, "line": {line},"column": {column},"message": {error} }] | ||
module.exports = function(data) { | ||
var output = []; | ||
const output = Object.keys(data).sort().reduce(function(res, name) { | ||
const errors = data[name]; | ||
Object.keys(data).sort().forEach(function(name) { | ||
var errors = data[name]; | ||
return res.concat(errors.map(function(entry) { | ||
const error = entry.error || entry; | ||
output = output.concat(errors.map(function(entry) { | ||
var error = entry.error || entry; | ||
return { | ||
@@ -17,8 +15,8 @@ name: name, | ||
message: entry.message, | ||
details: error.rawMessage ? error.message : null | ||
details: error.details || (error.rawMessage ? error.message : null) | ||
}; | ||
})); | ||
}); | ||
}, []); | ||
return JSON.stringify(output, null, 4); | ||
}; |
@@ -1,66 +0,48 @@ | ||
var fs = require('fs'); | ||
var path = require('path'); | ||
var csstree = require('css-tree'); | ||
var syntax = csstree.lexer; | ||
const csstree = require('css-tree'); | ||
const syntax = csstree.lexer; | ||
function defaultShouldBeValidated(filename) { | ||
return path.extname(filename) === '.css'; | ||
} | ||
function isTargetError(error) { | ||
if (!error) { | ||
return null; | ||
} | ||
function collectFiles(testPath, shouldBeValidated) { | ||
try { | ||
if (fs.statSync(testPath).isDirectory()) { | ||
return fs.readdirSync(testPath).reduce(function(result, dirFilename) { | ||
return result.concat(collectFiles(path.join(testPath, dirFilename), shouldBeValidated)); | ||
}, []); | ||
} else { | ||
return shouldBeValidated(testPath) ? [testPath] : []; | ||
} | ||
} catch (e) { | ||
return [testPath]; | ||
if (error.name !== 'SyntaxError' && | ||
error.name !== 'SyntaxMatchError' && | ||
error.name !== 'SyntaxReferenceError') { | ||
return null; | ||
} | ||
return error; | ||
} | ||
function validate(css, filename) { | ||
var errors = []; | ||
var ast = csstree.parse(css, { | ||
filename: filename, | ||
positions: true, | ||
onParseError: function(error) { | ||
errors.push(error); | ||
} | ||
}); | ||
function validateAtrule(node) { | ||
const atrule = node.name; | ||
const errors = []; | ||
let error; | ||
csstree.walk(ast, { | ||
visit: 'Declaration', | ||
enter: function(node) { | ||
var match = syntax.matchDeclaration(node); | ||
var error = match.error; | ||
if (error = isTargetError(syntax.checkAtruleName(atrule))) { | ||
errors.push(Object.assign(error, { | ||
...node.loc && node.loc.start | ||
})); | ||
return errors; | ||
} | ||
if (error) { | ||
var message = error.rawMessage || error.message || error; | ||
errors.push(...validateAtrulePrelude( | ||
atrule, | ||
node.prelude, | ||
(node.prelude && node.prelude.loc && node.prelude.loc.start) || (node.loc && node.loc.start) | ||
)); | ||
// ignore errors except those which make sense | ||
if (error.name !== 'SyntaxMatchError' && | ||
error.name !== 'SyntaxReferenceError') { | ||
return; | ||
} | ||
if (message === 'Mismatch') { | ||
message = 'Invalid value for `' + node.property + '`'; | ||
} | ||
errors.push({ | ||
name: error.name, | ||
node: node, | ||
loc: error.loc || node.loc, | ||
line: error.line || node.loc && node.loc.start && node.loc.start.line, | ||
column: error.column || node.loc && node.loc.start && node.loc.start.column, | ||
property: node.property, | ||
message: message, | ||
error: error | ||
}); | ||
if (node.block && node.block.children) { | ||
node.block.children.forEach(child => { | ||
if (child.type === 'Declaration') { | ||
errors.push(...validateAtruleDescriptor( | ||
atrule, | ||
child.property, | ||
child.value, | ||
child.loc && child.loc.start | ||
)); | ||
} | ||
} | ||
}); | ||
}); | ||
} | ||
@@ -70,83 +52,122 @@ return errors; | ||
function validateDictionary(dictionary) { | ||
var result = {}; | ||
function validateAtrulePrelude(atrule, prelude, preludeLoc) { | ||
const errors = []; | ||
let error; | ||
for (var filename in dictionary) { | ||
if (Object.prototype.hasOwnProperty.call(dictionary, filename)) { | ||
result[filename] = validate(dictionary[filename], filename); | ||
} | ||
if (error = isTargetError(syntax.checkAtrulePrelude(atrule, prelude))) { | ||
errors.push(Object.assign(error, { | ||
...preludeLoc || (prelude && prelude.loc && prelude.loc.start) | ||
})); | ||
} else if (error = isTargetError(syntax.matchAtrulePrelude(atrule, prelude).error)) { | ||
errors.push(Object.assign(error, { | ||
...error.rawMessage === 'Mismatch' && | ||
{ details: error.message, message: 'Invalid value for `@' + atrule + '` prelude' } | ||
})); | ||
} | ||
return result; | ||
return errors; | ||
} | ||
function validateString(css, filename) { | ||
var result = {}; | ||
function validateAtruleDescriptor(atrule, descriptor, value, descriptorLoc) { | ||
const errors = []; | ||
let error; | ||
if (!filename) { | ||
filename = '<unknown>'; | ||
if (error = isTargetError(syntax.checkAtruleDescriptorName(atrule, descriptor))) { | ||
errors.push(Object.assign(error, { | ||
atrule, | ||
descriptor, | ||
...descriptorLoc || (value && value.loc && value.loc.start) | ||
})); | ||
} else { | ||
if (error = isTargetError(syntax.matchAtruleDescriptor(atrule, descriptor, value).error)) { | ||
errors.push(Object.assign(error, { | ||
atrule, | ||
descriptor, | ||
...error.rawMessage === 'Mismatch' && | ||
{ details: error.message, message: 'Invalid value for `' + descriptor + '` descriptor' } | ||
})); | ||
} | ||
} | ||
result[filename] = validate(css, filename); | ||
return result; | ||
return errors; | ||
} | ||
function validateFile(filename) { | ||
var result = {}; | ||
var css; | ||
function validateDeclaration(property, value, valueLoc) { | ||
const errors = []; | ||
let error; | ||
try { | ||
css = fs.readFileSync(filename, 'utf-8'); | ||
result[filename] = validate(css, filename); | ||
} catch (e) { | ||
result[filename] = [e]; | ||
if (error = isTargetError(syntax.checkPropertyName(property))) { | ||
errors.push(Object.assign(error, { | ||
property, | ||
...valueLoc || (value && value.loc && value.loc.start) | ||
})); | ||
} else if (error = isTargetError(syntax.matchProperty(property, value).error)) { | ||
errors.push(Object.assign(error, { | ||
property, | ||
...error.rawMessage === 'Mismatch' && | ||
{ details: error.message, message: 'Invalid value for `' + property + '` property' } | ||
})); | ||
} | ||
return result; | ||
return errors; | ||
} | ||
function validateFileList(list) { | ||
return list.reduce(function(result, filename) { | ||
var res = validateFile(filename)[filename]; | ||
function validateRule(node) { | ||
const errors = []; | ||
if (res && res.length !== 0) { | ||
result[filename] = res; | ||
} | ||
if (node.block && node.block.children) { | ||
node.block.children.forEach(child => { | ||
if (child.type === 'Declaration') { | ||
errors.push(...validateDeclaration( | ||
child.property, | ||
child.value, | ||
child.loc && child.loc.start | ||
)); | ||
} | ||
}); | ||
} | ||
return result; | ||
}, {}); | ||
return errors; | ||
} | ||
function validatePath(searchPath, shouldBeValidated) { | ||
if (typeof shouldBeValidated !== 'function') { | ||
shouldBeValidated = defaultShouldBeValidated; | ||
} | ||
function validate(css, filename) { | ||
const errors = []; | ||
const ast = typeof css !== 'string' | ||
? css | ||
: csstree.parse(css, { | ||
filename, | ||
positions: true, | ||
parseAtrulePrelude: false, | ||
parseRulePrelude: false, | ||
parseValue: false, | ||
parseCustomProperty: false, | ||
onParseError(error) { | ||
errors.push(error); | ||
} | ||
}); | ||
return validateFileList(collectFiles(searchPath, shouldBeValidated)); | ||
} | ||
csstree.walk(ast, { | ||
visit: 'Atrule', | ||
enter(node) { | ||
errors.push(...validateAtrule(node)); | ||
} | ||
}); | ||
function validatePathList(pathList, shouldBeValidated) { | ||
if (typeof shouldBeValidated !== 'function') { | ||
shouldBeValidated = defaultShouldBeValidated; | ||
} | ||
csstree.walk(ast, { | ||
visit: 'Rule', | ||
enter(node) { | ||
errors.push(...validateRule(node)); | ||
} | ||
}); | ||
var fileList = Object.keys( | ||
pathList.reduce(function(result, searchPath) { | ||
collectFiles(searchPath, shouldBeValidated).forEach(function(filename) { | ||
result[filename] = true; | ||
}); | ||
return result; | ||
}, {}) | ||
); | ||
return validateFileList(fileList); | ||
return errors; | ||
}; | ||
module.exports = { | ||
validatePathList: validatePathList, | ||
validatePath: validatePath, | ||
validateFile: validateFile, | ||
validateDictionary: validateDictionary, | ||
validateString: validateString | ||
validateAtrule, | ||
validateAtrulePrelude, | ||
validateAtruleDescriptor, | ||
validateRule, | ||
validateDeclaration, | ||
validate | ||
}; |
{ | ||
"name": "csstree-validator", | ||
"version": "1.6.0", | ||
"description": "CSS validator build on csstree", | ||
"version": "2.0.0", | ||
"description": "CSS validator built on csstree", | ||
"author": "Roman Dvornov <rdvornov@gmail.com>", | ||
@@ -22,19 +22,30 @@ "license": "MIT", | ||
"test": "mocha --reporter dot", | ||
"travis": "npm run lint-and-test" | ||
"build": "rollup --config", | ||
"travis": "npm run lint-and-test", | ||
"prepublishOnly": "npm run build" | ||
}, | ||
"browser": { | ||
"css-tree": "css-tree/dist/csstree.min.js" | ||
}, | ||
"dependencies": { | ||
"clap": "^1.1.1", | ||
"css-tree": "^1.0.0" | ||
"css-tree": "^1.1.1" | ||
}, | ||
"devDependencies": { | ||
"@rollup/plugin-commonjs": "^11.0.2", | ||
"@rollup/plugin-json": "^4.0.2", | ||
"@rollup/plugin-node-resolve": "^7.1.1", | ||
"eslint": "^6.3.0", | ||
"mocha": "^5.2.0" | ||
"mocha": "^6.2.3", | ||
"rollup": "^1.32.1", | ||
"rollup-plugin-terser": "^5.3.0" | ||
}, | ||
"engines": { | ||
"node": ">=0.10.0" | ||
"node": ">=8.0.0" | ||
}, | ||
"files": [ | ||
"bin", | ||
"dist", | ||
"lib" | ||
] | ||
} |
@@ -6,6 +6,86 @@ [](https://www.npmjs.com/package/csstree-validator) | ||
CSS validator built on [CSSTree](https://github.com/csstree/csstree) | ||
## How to use: | ||
### NPM package | ||
```bash | ||
> npm i -g csstree-validator | ||
> npm install csstree-validator | ||
``` | ||
Manualy validate CSS string or [CSSTree's AST](https://github.com/csstree/csstree/blob/master/docs/ast.md): | ||
```js | ||
const { validate } = require('./lib'); | ||
console.log(validate('.class { pading: 10px; border: 1px super red }', 'demo/example.css')); | ||
// [ | ||
// SyntaxError [SyntaxReferenceError]: Unknown property `pading` { | ||
// reference: 'pading', | ||
// property: 'pading', | ||
// offset: 9, | ||
// line: 1, | ||
// column: 10 | ||
// }, | ||
// SyntaxError [SyntaxMatchError]: Mismatch { | ||
// message: 'Invalid value for `border` property', | ||
// rawMessage: 'Mismatch', | ||
// syntax: '<line-width> || <line-style> || <color>', | ||
// css: '1px super red', | ||
// mismatchOffset: 4, | ||
// mismatchLength: 5, | ||
// offset: 35, | ||
// line: 1, | ||
// column: 36, | ||
// loc: { source: 'demo/example.css', start: [Object], end: [Object] }, | ||
// property: 'border', | ||
// details: 'Mismatch\n' + | ||
// ' syntax: <line-width> || <line-style> || <color>\n' + | ||
// ' value: 1px super red\n' + | ||
// ' ------------^' | ||
// } | ||
// ] | ||
``` | ||
Another option is to use helpers to validate a file or directory and buildin reporters: | ||
```js | ||
const { validateFile } = require('csstree-validator'); | ||
const reporter = require('csstree-validator').reporters.checkstyle; | ||
console.log(reporter(validateFile('/path/to/style.css'))); | ||
``` | ||
#### API | ||
Validate methods: | ||
* validateAtrule(node) | ||
* validateAtrulePrelude(atrule, prelude, preludeLoc) | ||
* validateAtruleDescriptor(atrule, descriptor, value, descriptorLoc) | ||
* validateDeclaration(property, value, valueLoc) | ||
* validateRule(node) | ||
* validate(css, filename) | ||
Helpers: | ||
* validateDictionary(dictionary) | ||
* validateString(css, filename) | ||
* validateFile(filename) | ||
* validateFileList(list) | ||
* validatePath(searchPath, filter) | ||
* validatePathList(pathList, filter) | ||
Reporters | ||
* json | ||
* console | ||
* checkstyle | ||
* gnu | ||
### CLI (terminal command) | ||
```bash | ||
> npm install -g csstree-validator | ||
> csstree-validator /path/to/style.css | ||
@@ -27,14 +107,5 @@ ``` | ||
## API | ||
```js | ||
var validateFile = require('csstree-validator').validateFile; | ||
var reporter = require('csstree-validator').reporters.checkstyle; | ||
console.log(reporter(validateFile('/path/to/style.css'))); | ||
``` | ||
## Ready to use | ||
Some plugins that are using `csstree-validator`: | ||
Plugins that are using `csstree-validator`: | ||
@@ -41,0 +112,0 @@ * [Sublime plugin](https://github.com/csstree/SublimeLinter-contrib-csstree) |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
191037
15
1034
119
7
1
Updatedcss-tree@^1.1.1