colorguard
Advanced tools
Comparing version 0.3.0 to 1.0.0
@@ -1,15 +0,9 @@ | ||
var walk = require('rework-walk'); | ||
var rework = require('rework'); | ||
var assign = require('object-assign'); | ||
var colorDiff = require('color-diff'); | ||
var formatter = require('./formatter'); | ||
var postcss = require('postcss'); | ||
var pipetteur = require('pipetteur'); | ||
var colorDiff = require('color-diff'); | ||
var SourceMapConsumer = require('source-map').SourceMapConsumer; | ||
var colors = {}; | ||
var sourceMapConsumer; | ||
var reporter = require('postcss-reporter'); | ||
function getWhitelistHashKey(pair) { | ||
pair = pair.sort(); | ||
return pair[0] + '-' + pair[1]; | ||
} | ||
function convertToLab(clr) { | ||
function convertToLab (clr) { | ||
clr = clr.rgb(); | ||
@@ -31,45 +25,28 @@ | ||
// we need line position instead | ||
function getColumnPositionRelativeToLine(position, which) { | ||
return position.declaration.position[which].column + position.column + position.declaration.property.length; | ||
function getColumnPositionRelativeToLine (position) { | ||
var decl = position.declaration; | ||
return decl.source.start.column + position.column + decl.prop.length; | ||
} | ||
function renderOriginalPosition (position) { | ||
return position.source + ':' + position.line + ':' + position.column; | ||
function getWhitelistHashKey (pair) { | ||
pair = pair.sort(); | ||
return pair[0] + '-' + pair[1]; | ||
} | ||
function renderConflictLine (data) { | ||
return '_match_ (_hex_) [line: _lines_]' | ||
.replace('_match_', data.lines[0].match) | ||
.replace('_hex_', data.lines[0].color.hex()) | ||
.replace('_lines_', data.lines.map(function (info, index) { | ||
var result = info.declaration.position.start.line + ':' + getColumnPositionRelativeToLine(info, 'start'); | ||
if (data.originalLines && data.originalLines.length) { | ||
result += ' (' + renderOriginalPosition(data.originalLines[index]) + ')'; | ||
} | ||
return result; | ||
}).join(', ')); | ||
function getDiff (a, b) { | ||
return Math.min(colorDiff.diff(convertToLab(a), convertToLab(b)), 100); | ||
} | ||
function getOriginalPosition (info) { | ||
return sourceMapConsumer && sourceMapConsumer.originalPositionFor({ | ||
line: info.declaration.position.start.line, | ||
column: getColumnPositionRelativeToLine(info, 'start') | ||
}); | ||
} | ||
var colorguard = postcss.plugin('css-colorguard', function (opts) { | ||
opts = assign({ | ||
ignore: [], | ||
threshold: 3 | ||
}, opts); | ||
exports.inspect = function (css, options, map) { | ||
options = options || {}; | ||
colors = {}; | ||
if (map) { | ||
sourceMapConsumer = new SourceMapConsumer(map); | ||
} | ||
var threshold = typeof options.threshold !== 'undefined' ? options.threshold : 3; | ||
options.ignore = options.ignore || []; | ||
var whitelistHash = {}; | ||
if (options.whitelist) { | ||
options.whitelist.forEach(function (pair) { | ||
if (opts.whitelist) { | ||
opts.whitelist.forEach(function (pair) { | ||
if (!Array.isArray(pair)) { | ||
throw new Error('The whitelist option takes an array of array pairs. You probably sent an array of strings.'); | ||
throw new Error('The whitelist option takes an array of array pairs. ' + | ||
'You probably sent an array of strings.'); | ||
} | ||
@@ -80,117 +57,51 @@ whitelistHash[getWhitelistHashKey(pair)] = true; | ||
// In this section, we more or less ruin the actual css, but not for our purposes. The following | ||
// changes are necessary for the parser to not barf at us. We'll need to undo this if we ever | ||
// wanted to use the rework output. For now, it's fine as long as we keep the line numbers the | ||
// same. | ||
return function (css, result) { | ||
var colors = {}; | ||
// https://github.com/SlexAxton/css-colorguard/issues/2 | ||
css.walkDecls(function (decl) { | ||
var matches = pipetteur(decl.value); | ||
matches.forEach(function (match) { | ||
// FIXME: This discards alpha channel | ||
var name = match.color.hex(); | ||
// Just bail if we want to ignore the color | ||
if (opts.ignore.indexOf(name) > -1) { | ||
return; | ||
} | ||
css = css.replace(/url\((.*?#.*?)\)/ig, function (match, content) { | ||
// Capture the content in order to replace with a similar length string | ||
// This allows us to keep the column numer correct | ||
return match.replace(content, new Array(content.length + 1).join('_')); | ||
}); | ||
match.declaration = decl; | ||
// Run rework over it so we can parse out all the colors | ||
// rework(css).use(findColors()).toString(); | ||
var workingTree = rework(css).use(function (style) { | ||
walk(style, function (rule) { | ||
if (rule.declarations) { | ||
rule.declarations.forEach(function (declaration) { | ||
if (declaration.type === 'declaration') { | ||
var matches = pipetteur(declaration.value); | ||
if (!(name in colors)) { | ||
colors[name] = colors[name] || []; | ||
colors[name].push(match); | ||
} | ||
if (matches.length) { | ||
matches.forEach(function (match) { | ||
// FIXME: This discards alpha channel | ||
var name = match.color.hex(); | ||
Object.keys(colors).forEach(function (color) { | ||
var cached = colors[color]; | ||
if (cached[0] === match || cached[0].match === match.match) { | ||
return; | ||
} | ||
var diffAmount = getDiff(cached[0].color, match.color); | ||
match.declaration = declaration; | ||
colors[name] = colors[name] || []; | ||
colors[name].push(match); | ||
}); | ||
} | ||
var whitelisted = getWhitelistHashKey([color, name]); | ||
if (diffAmount < opts.threshold && !whitelistHash[whitelisted]) { | ||
decl.warn(result, match.match + ' collides with ' + | ||
cached[0].match + | ||
' (' + | ||
cached[0].declaration.source.start.line + | ||
':' + | ||
getColumnPositionRelativeToLine(cached[0]) + | ||
')'); | ||
} | ||
}); | ||
} | ||
}); | ||
}); | ||
}); | ||
var colorNames = Object.keys(colors); | ||
var colorLen = colorNames.length; | ||
var c1; | ||
var c2; | ||
var diffAmount; | ||
var infoBlock; | ||
// Mock the output structure | ||
var output = { | ||
collisions: [], | ||
info: [], | ||
stats: { | ||
counts: {} | ||
} | ||
}; | ||
}); | ||
// Set the total number of different colors | ||
output.stats.total = colorLen; | ||
colorguard.process = function (css, opts) { | ||
opts = opts || {}; | ||
var processor = postcss([ colorguard(opts), reporter({formatter: formatter}) ]); | ||
return processor.process(css, opts); | ||
}; | ||
// Generate the stats for the colors | ||
colorNames.forEach(function (colorName) { | ||
// Counts of colors | ||
output.stats.counts[colorName] = colors[colorName].length; | ||
}); | ||
// Loop over the object but avoid duplicates and collisions | ||
for (var i = 0; i < colorLen; i += 1) { | ||
// Just bail if we want to ignore this color altogether | ||
if (options.ignore.indexOf(colorNames[i]) >= 0) { | ||
continue; | ||
} | ||
for (var j = i + 1; j < colorLen; j += 1) { | ||
if (options.ignore.indexOf(colorNames[j]) >= 0) { | ||
continue; | ||
} | ||
// Convert each to rgb format | ||
c1 = colors[colorNames[i]]; | ||
c2 = colors[colorNames[j]]; | ||
// Avoid greater than 100 values | ||
diffAmount = Math.min(colorDiff.diff(convertToLab(c1[0].color), convertToLab(c2[0].color)), 100); | ||
// All distances go into the info block | ||
infoBlock = { | ||
colors: [{ | ||
rgb: colorNames[i], | ||
lines: c1, | ||
originalLines: sourceMapConsumer && c1.map(getOriginalPosition) || [] | ||
}, { | ||
rgb: colorNames[j], | ||
lines: c2, | ||
originalLines: sourceMapConsumer && c2.map(getOriginalPosition) || [] | ||
}], | ||
distance: diffAmount | ||
}; | ||
// push it onto the block | ||
output.info.push(infoBlock); | ||
// If the amount is less than the threshold, and if the two colors aren't whitelisted, then | ||
// we have a collision | ||
if (diffAmount < threshold && !whitelistHash[getWhitelistHashKey([colorNames[i], colorNames[j]])]) { | ||
infoBlock.message = '_1_ is too close (_diffAmount_) to _2_' | ||
.replace('_1_', renderConflictLine(infoBlock.colors[0])) | ||
.replace('_2_', renderConflictLine(infoBlock.colors[1])) | ||
.replace('_diffAmount_', diffAmount); | ||
output.collisions.push(infoBlock); | ||
} | ||
} | ||
} | ||
return output; | ||
}; | ||
module.exports = colorguard; |
{ | ||
"name": "colorguard", | ||
"version": "0.3.0", | ||
"version": "1.0.0", | ||
"description": "Keep a watchful eye on your css colors", | ||
@@ -8,3 +8,3 @@ "main": "index.js", | ||
"scripts": { | ||
"test": "mocha --require should --reporter spec" | ||
"test": "tape test/*.js | tap-spec" | ||
}, | ||
@@ -26,3 +26,4 @@ "files": [ | ||
"csslint", | ||
"rework" | ||
"postcss", | ||
"postcss-plugin" | ||
], | ||
@@ -36,14 +37,17 @@ "author": "Alex Sexton <alexsexton@gmail.com>", | ||
"dependencies": { | ||
"chalk": "^1.1.1", | ||
"color-diff": "^0.1.3", | ||
"log-symbols": "^1.0.2", | ||
"object-assign": "^4.0.1", | ||
"pipetteur": "^1.0.1", | ||
"rework": "^1.0.0", | ||
"rework-walk": "^1.0.0", | ||
"source-map": "^0.2.0", | ||
"source-map-resolve": "^0.3.1", | ||
"plur": "^2.0.0", | ||
"postcss": "^5.0.4", | ||
"postcss-reporter": "^1.2.1", | ||
"text-table": "^0.2.0", | ||
"yargs": "^1.2.6" | ||
}, | ||
"devDependencies": { | ||
"mocha": "^1.20.1", | ||
"should": "^4.0.4" | ||
"tap-spec": "^4.1.0", | ||
"tape": "^4.2.0" | ||
} | ||
} |
152
README.md
@@ -10,78 +10,63 @@ [![Build Status](https://travis-ci.org/SlexAxton/css-colorguard.svg?branch=master)](https://travis-ci.org/SlexAxton/css-colorguard) | ||
## Usage | ||
## How it works | ||
Generally, you'll want to do this after you've run things through your css preprocessor, so variables | ||
and other preprocessor specific things are out of the way. | ||
Colorguard uses the [CIEDE2000](http://en.wikipedia.org/wiki/Color_difference#CIEDE2000) algorithm to determine | ||
the similarity of each of the colors in your CSS file. This algorithm is quite complex, but is used | ||
in the broadcasting community as the best approximation of human ability to discern differences in | ||
color. RGB on the other hand, is pretty bad at representing differences in color purely based on the | ||
numerical difference of the hex values. | ||
### Command Line | ||
Luckily, [someone else already implemented CIEDE2000](https://github.com/markusn/color-diff), so I | ||
didn't have to. Tight. Cause this thing is mathy as hell. | ||
```bash | ||
$ npm install -g colorguard | ||
``` | ||
![http://f.cl.ly/items/061h1y0x0G2X2e2t1q1f/Screen%20Shot%202014-07-03%20at%205.55.17%20PM.png](http://f.cl.ly/items/061h1y0x0G2X2e2t1q1f/Screen%20Shot%202014-07-03%20at%205.55.17%20PM.png) | ||
### Alpha Transparency | ||
```bash | ||
# Just regular | ||
colorguard --file style.css | ||
Currently, alpha transparency is just stripped from the colors. So `rgb(0, 0, 0)` exactly matches | ||
`rgba(0,0,0,0.5)`. This is usually fine unless someone is alphatransparency-happy and uses it for | ||
darkening and lightening colors too often. It could probably be its own check in the future that | ||
there aren't too many different alpha transparencies of the same color. This is not currently a | ||
thing though. | ||
# pipe a file | ||
cat file.css | colorguard | ||
## API | ||
# Threshold is available via command line | ||
colorguard --file style.css --threshold 3 | ||
### `colorguard.process(css, [options]).then(function(result) {})` | ||
# The other options are too hard to type, so just pass it a json object | ||
# with `ignore` or `whitelist` properties (overrides `--threshold option`) | ||
colorguard --file style.css --options colorguard.json | ||
#### options | ||
# Change the output type to full json (includes stats) | ||
colorguard --file style.css --format json | ||
``` | ||
##### ignore | ||
Example output | ||
Type: `array` | ||
```bash | ||
$ colorguard --file test/fixtures/simple.css | ||
Collision: #020202, #000000 | ||
- #020202 [line: 2] is too close (0.3146196209793196) to #000000 [line: 2, 3, 7, 12, 13, 16, 17] | ||
Collision: #020202, #010101 | ||
- #020202 [line: 2] is too close (0.1574963682909058) to #010101 [line: 20] | ||
Collision: #000000, #010101 | ||
- #000000 [line: 2, 3, 7, 12, 13, 16, 17] is too close (0.15712369811016996) to #010101 [line: 20] | ||
``` | ||
Specify hex codes of colors that you would like to ignore completely. | ||
Use with caution. | ||
```bash | ||
$ cat test/fixtures/simple.css | colorguard --format json | ||
{"collisions":[{"colors":[{"rgb":"#020202","lines":[2]},{"rgb":"#000000","lines":[2,3,7,12,13,16,17]}],"distance":0.3146196209793196,"message":"#020202 [line: 2] is too close (0.3146196209793196) to #000000 [line: 2, 3, 7, 12, 13, 16, 17]"},{"colors":[{"rgb":"#020202","lines":[2]},{"rgb":"#010101","lines":[20]}],"distance":0.1574963682909058,"message":"#020202 [line: 2] is too close (0.1574963682909058) to #010101 [line: 20]"},{"colors":[{"rgb":"#000000","lines":[2,3,7,12,13,16,17]},{"rgb":"#010101","lines":[20]}],"distance":0.15712369811016996,"message":"#000000 [line: 2, 3, 7, 12, 13, 16, 17] is too close (0.15712369811016996) to #010101 [line: 20]"}],"info":[{"colors":[{"rgb":"#020202","lines":[2]},{"rgb":"#000000","lines":[2,3,7,12,13,16,17]}],"distance":0.3146196209793196,"message":"#020202 [line: 2] is too close (0.3146196209793196) to #000000 [line: 2, 3, 7, 12, 13, 16, 17]"},{"colors":[{"rgb":"#020202","lines":[2]},{"rgb":"#663399","lines":[9]}],"distance":34.12252478659537},{"colors":[{"rgb":"#020202","lines":[2]},{"rgb":"#010101","lines":[20]}],"distance":0.1574963682909058,"message":"#020202 [line: 2] is too close (0.1574963682909058) to #010101 [line: 20]"},{"colors":[{"rgb":"#020202","lines":[2]},{"rgb":"#FFFFFF","lines":[21]}],"distance":99.42663222854084},{"colors":[{"rgb":"#000000","lines":[2,3,7,12,13,16,17]},{"rgb":"#663399","lines":[9]}],"distance":34.321183445222175},{"colors":[{"rgb":"#000000","lines":[2,3,7,12,13,16,17]},{"rgb":"#010101","lines":[20]}],"distance":0.15712369811016996,"message":"#000000 [line: 2, 3, 7, 12, 13, 16, 17] is too close (0.15712369811016996) to #010101 [line: 20]"},{"colors":[{"rgb":"#000000","lines":[2,3,7,12,13,16,17]},{"rgb":"#FFFFFF","lines":[21]}],"distance":100},{"colors":[{"rgb":"#663399","lines":[9]},{"rgb":"#010101","lines":[20]}],"distance":34.22102591917981},{"colors":[{"rgb":"#663399","lines":[9]},{"rgb":"#FFFFFF","lines":[21]}],"distance":60.25283160954553},{"colors":[{"rgb":"#010101","lines":[20]},{"rgb":"#FFFFFF","lines":[21]}],"distance":99.7195446868893}],"stats":{"counts":{"#020202":1,"#000000":7,"#663399":1,"#010101":1,"#FFFFFF":1},"total":5}} | ||
``` | ||
##### threshold | ||
Type: `number` | ||
Default: `3` | ||
### Programmatic | ||
`0` through `100`. Lower values are more precise; the default is `3` but that's | ||
mostly personal opinion. | ||
```bash | ||
$ npm install --save-dev colorguard | ||
``` | ||
##### whitelist | ||
```javascript | ||
var colorguard = require('colorguard'); | ||
var fs = require('fs'); | ||
Type: `array` | ||
var css = fs.readFileSync('./file.css', 'utf8'); | ||
Pass an array of color pairs to ignore: | ||
var output = colorguard.inspect(css, { | ||
// 0 through 100. Lower is more similar. Anything below 3 warns you. | ||
// 3 is the default threshold, but that's mostly personal opinion | ||
threshold: 3, | ||
```js | ||
[['#000000', '#010101']] | ||
``` | ||
// This color is just ignored entirely (use with caution) | ||
ignore: ["#030303"], | ||
### `postcss([ colorguard(opts) ])` | ||
// These color combinations are ignored (usually use this) | ||
whitelist: [["#000000", "#010101"]] | ||
}); | ||
``` | ||
CSS Colorguard can be consumed as a PostCSS plugin. See the | ||
[documentation](https://github.com/postcss/postcss#usage) for examples for | ||
your environment. | ||
### Build Time | ||
CSS Colorguard can also be used in conjunction with other javascript build systems, such as: | ||
CSS Colorguard can be used in conjunction with other javascript build systems, such as: | ||
@@ -92,62 +77,27 @@ * [gulp-colorguard](https://github.com/pgilad/gulp-colorguard) | ||
### CLI | ||
## The Output | ||
CSS Colorguard also ships with a CLI app. To see the available options, just run: | ||
You'll get warnings back (as an object via js or if the format is set to `json`), as well as some | ||
additional color stats. Those are just for fun or whatever. | ||
```json | ||
{ | ||
"collisions": [ | ||
{ | ||
"colors": [ | ||
{ | ||
"rgb": "#010101", | ||
"lines": [23, 45, 234] | ||
}, | ||
{ | ||
"rgb": "#020202", | ||
"lines": [29] | ||
} | ||
], | ||
"distance": 0.1574963682909058, | ||
"message": "#010101 [line: 23, 45, 234] is too close (0.1574963682909058) to #020202 [line: 29]." | ||
} | ||
], | ||
"stats": { | ||
"counts": { | ||
"#010101": 3, | ||
"#020202": 1, | ||
"#030303": 0 | ||
}, | ||
"total": 3 | ||
} | ||
} | ||
```bash | ||
$ colorguard --help | ||
``` | ||
## How it works | ||
## Install | ||
Colorguard uses the [CIEDE2000](http://en.wikipedia.org/wiki/Color_difference#CIEDE2000) algorithm to determine | ||
the similarity of each of the colors in your CSS file. This algorithm is quite complex, but is used | ||
in the broadcasting community as the best approximation of human ability to discern differences in | ||
color. RGB on the other hand, is pretty bad at representing differences in color purely based on the | ||
numerical difference of the hex values. | ||
With npm, to get the command do: | ||
Luckily, [someone else already implemented CIEDE2000](https://github.com/markusn/color-diff), so I | ||
didn't have to. Tight. Cause this thing is mathy as hell. | ||
```bash | ||
npm install -g colorguard | ||
``` | ||
![http://f.cl.ly/items/061h1y0x0G2X2e2t1q1f/Screen%20Shot%202014-07-03%20at%205.55.17%20PM.png](http://f.cl.ly/items/061h1y0x0G2X2e2t1q1f/Screen%20Shot%202014-07-03%20at%205.55.17%20PM.png) | ||
To get the library & PostCSS plugin, do: | ||
### Alpha Transparency | ||
```bash | ||
npm install colorguard | ||
``` | ||
Currently, alpha transparency is just stripped from the colors. So `rgb(0, 0, 0)` exactly matches | ||
`rgba(0,0,0,0.5)`. This is usually fine unless someone is alphatransparency-happy and uses it for | ||
darkening and lightening colors too often. It could probably be it's own check in the future that | ||
there aren't too many different alpha transparencies of the same color. This is not currently a | ||
thing though. | ||
## Thanks | ||
* [Stripe](https://stripe.com/) - They let me build this at work | ||
* [reworkcss](https://github.com/reworkcss) - Makes this work | ||
* [@markusn](https://github.com/markusn) - Best CIEDE2000 implementation ever | ||
@@ -154,0 +104,0 @@ |
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
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
6
0
10500
10
128
116
1
+ Addedchalk@^1.1.1
+ Addedlog-symbols@^1.0.2
+ Addedobject-assign@^4.0.1
+ Addedplur@^2.0.0
+ Addedpostcss@^5.0.4
+ Addedpostcss-reporter@^1.2.1
+ Addedtext-table@^0.2.0
+ Addedansi-regex@2.1.1(transitive)
+ Addedansi-styles@2.2.1(transitive)
+ Addedchalk@1.1.3(transitive)
+ Addedescape-string-regexp@1.0.5(transitive)
+ Addedhas-ansi@2.0.0(transitive)
+ Addedhas-flag@1.0.0(transitive)
+ Addedirregular-plurals@1.4.0(transitive)
+ Addedjs-base64@2.6.4(transitive)
+ Addedlodash@4.17.21(transitive)
+ Addedlog-symbols@1.0.2(transitive)
+ Addedobject-assign@4.1.1(transitive)
+ Addedplur@2.1.2(transitive)
+ Addedpostcss@5.2.18(transitive)
+ Addedpostcss-reporter@1.4.1(transitive)
+ Addedsource-map@0.5.7(transitive)
+ Addedstrip-ansi@3.0.1(transitive)
+ Addedsupports-color@2.0.03.2.3(transitive)
+ Addedtext-table@0.2.0(transitive)
- Removedrework@^1.0.0
- Removedrework-walk@^1.0.0
- Removedsource-map@^0.2.0
- Removedsource-map-resolve@^0.3.1
- Removedamdefine@1.0.1(transitive)
- Removedatob@1.1.32.1.2(transitive)
- Removedconvert-source-map@0.3.5(transitive)
- Removedcss@2.2.4(transitive)
- Removeddecode-uri-component@0.2.2(transitive)
- Removedinherits@2.0.4(transitive)
- Removedresolve-url@0.2.1(transitive)
- Removedrework@1.0.1(transitive)
- Removedrework-walk@1.0.0(transitive)
- Removedsource-map@0.2.00.6.1(transitive)
- Removedsource-map-resolve@0.3.10.5.3(transitive)
- Removedsource-map-url@0.3.00.4.1(transitive)
- Removedurix@0.1.0(transitive)