Skip to content

Commit 8a36b97

Browse files
committed
Add --similar-colors flag to remove diffs that show colors changed are within 1 r g and b of each other.
1 parent 424a20f commit 8a36b97

File tree

15 files changed

+249
-7
lines changed

15 files changed

+249
-7
lines changed

bin/css-compare

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ var argv = yargs.usage('Normalizes and compares CSS output.')
3232

3333
.describe('context', 'Provide diff context.')
3434

35+
.describe('similar-colors', 'Excludes diffs for similar colors (rgb each within 1).')
36+
3537
.argv;
3638

3739
var diff = csscomp(argv.test, argv.control, argv).diff;

css-compare.js

+166-7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ var fs = require('fs');
55
var color = require('color-parser');
66
var diff = require('diff');
77
var _ = require('lodash');
8+
var util = require('util');
89

910
function parseColors(value) {
1011
// Throw everything at colorString, and see what sticks.
@@ -141,15 +142,173 @@ var extensions = {
141142
removeprefixes: require('./lib/extensions/removeprefixes')
142143
};
143144

144-
function createDiff(fileName, oldStr, newStr, options) {
145-
return diff.createPatch(fileName, oldStr, newStr).split("\n").filter(function(line) {
146-
if (options.context) {
147-
return true;
145+
function filterContext(lines) {
146+
return lines.filter(function(line) {
147+
const trimmed = line.trimRight();
148+
return trimmed !== "+" &&
149+
trimmed !== "-" &&
150+
(trimmed.indexOf('-') === 0 || trimmed.indexOf('+') === 0);
151+
});
152+
}
153+
154+
function filterSimilarColors(patchLines) {
155+
function hasPlusLinesDirectlyAhead(index) {
156+
for(var i = index; i < patchLines.length; i++) {
157+
if (patchLines[i].startsWith("-")) {
158+
continue;
159+
}
160+
161+
return patchLines[i].startsWith("+") && !patchLines[i].startsWith("+++");
148162
}
149163

150-
const trimmed = line.trimRight();
151-
return trimmed !== "+" && trimmed !== "-" && (trimmed.indexOf('-') === 0 || trimmed.indexOf('+') === 0)
152-
}).join("\n");
164+
return false;
165+
}
166+
167+
// Batch diffs into chunks while maintaining line numbers.
168+
// [
169+
// {
170+
// 'old': [
171+
// {line: 34, text: '- opacity: 1.0'}
172+
// {line: 35, text: '- color: rgba(0, 0, 0, 0)'}
173+
// ],
174+
// 'new': [
175+
// {line: 36, text: '+ opacity: 0.9'}
176+
// {line: 37, text: '+ color: rgba(1, 1, 1, 0)'}
177+
// ],
178+
// },
179+
// ...
180+
// ]
181+
let oldLines = [];
182+
let newLines = [];
183+
let chunks = [];
184+
patchLines.forEach((line, index) => {
185+
// Only accumulate old lines if we peek ahead and see new lines coming.
186+
// We don't want to accumulate single removals.
187+
if (line.startsWith("-") && !line.startsWith("---") && hasPlusLinesDirectlyAhead(index)) {
188+
oldLines.push({line: index, text: line});
189+
return;
190+
}
191+
192+
// Only accumulate new lines if we found old lines. We don't want to
193+
// accumulate single additions.
194+
if (line.startsWith("+") && !line.startsWith("+++") && oldLines.length > 0) {
195+
newLines.push({line: index, text: line});
196+
}
197+
198+
// Check if we've completed a chunk
199+
if (oldLines.length > 0 && newLines.length > 0 &&
200+
(index === patchLines.length - 1 || !patchLines[index + 1].startsWith("+"))) {
201+
// We completed a chunk. Accumulate it.
202+
chunks.push(
203+
{
204+
'old': oldLines,
205+
'new': newLines
206+
}
207+
);
208+
// Reset our state
209+
oldLines = [];
210+
newLines = [];
211+
}
212+
});
213+
214+
// Filter our lines that don't include rgba. Whitelist what property values
215+
// can be.
216+
// [
217+
// {
218+
// 'old': [
219+
// {line: 35, text: '- color: rgba(0, 0, 0, 0)'}
220+
// ],
221+
// 'new': [
222+
// {line: 37, text: '+ color: rgba(1, 1, 1, 0)'}
223+
// ],
224+
// },
225+
// ...
226+
// ]
227+
const ALLOWED_PROPERTIES = [
228+
"color",
229+
"background-color",
230+
];
231+
const lineFilter = (line) => {
232+
const rgbaCount = (line.text.match(/rgba\(/g) || []).length;
233+
const match = line.text.match(/[+-][ ]+([^:]+):/);
234+
235+
if (rgbaCount > 1) {
236+
throw new Error("More than one rgba per line. Investigate!");
237+
}
238+
239+
return rgbaCount === 1 &&
240+
(match && ALLOWED_PROPERTIES.includes(match[1]));
241+
}
242+
243+
chunks.forEach((chunk) => {
244+
chunk.old = chunk.old.filter(lineFilter);
245+
chunk.new = chunk.new.filter(lineFilter);
246+
});
247+
248+
chunks = chunks.filter((chunk) => {
249+
return chunk.old.length > 0 && chunk.new.length > 0;
250+
});
251+
252+
// Compare old and new by index. If the colors are the same within a tolerance
253+
// keep track of what lines to remove
254+
// [35, 37]
255+
const linesToRemove = [];
256+
257+
chunks.forEach((chunk) => {
258+
for (var i = 0; i < chunk.old.length && i < chunk.new.length; i++) {
259+
const oldLine = chunk.old[i].text;
260+
const newLine = chunk.new[i].text;
261+
262+
const oldRgbRegex = /rgba\(([\d]+), ([\d]+), ([\d]+),/g;
263+
const oldMatch = oldRgbRegex.exec(oldLine);
264+
const newRgbRegex = /rgba\(([\d]+), ([\d]+), ([\d]+),/g;
265+
const newMatch = newRgbRegex.exec(newLine);
266+
267+
if (!oldMatch || !newMatch) {
268+
throw new Error("Expected rgba to exist on these lines!");
269+
}
270+
271+
const oldR = parseInt(oldMatch[1], 10);
272+
const oldG = parseInt(oldMatch[2], 10);
273+
const oldB = parseInt(oldMatch[3], 10);
274+
275+
const newR = parseInt(newMatch[1], 10);
276+
const newG = parseInt(newMatch[2], 10);
277+
const newB = parseInt(newMatch[3], 10);
278+
279+
if (Math.abs(oldR - newR) <= 1 &&
280+
Math.abs(oldG - newG) <= 1 &&
281+
Math.abs(oldB - newB) <= 1) {
282+
linesToRemove.push(chunk.old[i].line);
283+
linesToRemove.push(chunk.new[i].line);
284+
}
285+
}
286+
});
287+
288+
// Sort the line numbers and remove in reverse
289+
linesToRemove.sort((a,b) => a - b);
290+
291+
for (var i = linesToRemove.length - 1; i >= 0; i--) {
292+
patchLines.splice(linesToRemove[i], 1);
293+
}
294+
295+
return patchLines;
296+
}
297+
298+
function createDiff(fileName, oldStr, newStr, options) {
299+
let patchLines = diff.createPatch(fileName, oldStr, newStr).split("\n");
300+
301+
// This should be done before context removal because it helps us
302+
// differentiate between chunks of differences
303+
if (options["similar-colors"]) {
304+
patchLines = filterSimilarColors(patchLines);
305+
}
306+
307+
if (!options.context) {
308+
patchLines = filterContext(patchLines);
309+
}
310+
311+
return patchLines.join("\n");
153312
}
154313

155314
module.exports = function(test, control, options){

spec/csscompSpec.js

+25
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ function fixture(filename) {
1313
return fs.realpathSync('./spec/fixtures/'+filename);
1414
}
1515

16+
function diffFixtures(dir, options) {
17+
var options = options || {};
18+
var diff = csscomp(fixture(dir+files.test), fixture(dir+files.control), Object.assign(options, {label: files.test})).diff;
19+
var expected = fs.readFileSync(fixture(dir+files.expected)).toString();
20+
expect(diff).toBe(expected);
21+
}
22+
1623
describe("Comparisons", function() {
1724
it("should compare", function() {
1825
var diff = csscomp(fixture('general/'+files.test), fixture('general/'+files.control), {label: files.test}).diff;
@@ -43,3 +50,21 @@ describe("Extensions", function(){
4350

4451
});
4552
});
53+
54+
describe("Colors", function(){
55+
it("should be filtered from the diff if the rgba values are the same", function(){
56+
diffFixtures('similar-colors/same/', {"similar-colors": true});
57+
});
58+
59+
it("should not be filtered from the diff if the rgba values are outside a specified tolerance", function(){
60+
diffFixtures('similar-colors/different/', {"similar-colors": true});
61+
});
62+
63+
it("should be filtered from the diff if the rgba values are within a specified tolerance", function(){
64+
diffFixtures('similar-colors/within-tolerance/', {"similar-colors": true});
65+
});
66+
67+
it("should be filtered from any property name", function(){
68+
diffFixtures('similar-colors/names/', {"similar-colors": true});
69+
});
70+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
h1 {
2+
opacity: 1.0;
3+
color: rgba(127, 127, 127, 0);
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
--- test.css
2+
+++ test.css
3+
- opacity: 1;
4+
- color: rgba(127, 127, 127, 0);
5+
+ opacity: 0.9;
6+
+ color: rgba(127, 127, 129, 0);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
h1 {
2+
opacity: 0.9;
3+
color: rgba(127, 127, 129, 0);
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
h1 {
2+
opacity: 1.0;
3+
color: rgba(127, 127, 127, 0);
4+
background-color: rgba(127, 127, 127, 0);
5+
foo: rgba(127, 127, 127, 0);
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
--- test.css
2+
+++ test.css
3+
- opacity: 1;
4+
- foo: rgba(127, 127, 127, 0);
5+
+ opacity: 0.9;
6+
+ foo: rgba(128, 128, 128, 0);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
h1 {
2+
opacity: 0.9;
3+
color: rgba(128, 128, 128, 0);
4+
background-color: rgba(128, 128, 128, 0);
5+
foo: rgba(128, 128, 128, 0);
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
h1 {
2+
opacity: 1.0;
3+
color: rgba(127, 127, 127, 0);
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
--- test.css
2+
+++ test.css
3+
- opacity: 1;
4+
+ opacity: 0.9;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
h1 {
2+
opacity: 0.9;
3+
color: rgba(127, 127, 127, 0);
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
h1 {
2+
opacity: 1.0;
3+
color: rgba(127, 127, 127, 0);
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
--- test.css
2+
+++ test.css
3+
- opacity: 1;
4+
+ opacity: 0.9;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
h1 {
2+
opacity: 0.9;
3+
color: rgba(128, 128, 128, 0);
4+
}

0 commit comments

Comments
 (0)