├── .gitignore ├── CHANGES ├── LICENSE ├── README.rst ├── assets └── cssdiff-icon.ai ├── devtools.html ├── devtools.js ├── icon-128.png ├── icon-48.png ├── make-zip.sh ├── manifest.json └── panel ├── diff.js ├── index.html ├── mustache.js ├── panel.css └── panel.js /.gitignore: -------------------------------------------------------------------------------- 1 | # vim swap files 2 | .*.sw* 3 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Version 1.0 2 | Released: December 1st, 2013 3 | 4 | * Initial release 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Evan Borgstrom 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | CSS Diff Chrome Extension 2 | ========================== 3 | This is a Chrome Devtools Extension that tracks changes you make via the 4 | inspector and provides a convenient "patch" style listing of the changes so 5 | that they can be easily shared. 6 | 7 | .. image:: http://i.imgur.com/QNshLFB.png 8 | :alt: Money Shot 9 | 10 | This grew out of our own frustration with how we worked with designers or art 11 | directors. Typically we'll get a screenshot emailed to you highlighting the 12 | changes they want made so that their layouts are "pixel perfect". 13 | 14 | Once these design types are introduced to the inspector they find joy in the 15 | ability to endlessly play & tweak values, but we found that there was still no 16 | good way to convey the changes they made back to us. We started getting email 17 | with screenshots of the inspector highlighting the changes, a little better 18 | than just a marked up screenshot but still a pain to merge the changes back 19 | into the code base. 20 | 21 | We investigated all of the live-edit style CSS systems, but they're all so 22 | much overkill for what we need and require lots of complicated setup. They 23 | just wouldn't work for design types. 24 | 25 | After discussing the problem around the studio for a couple weeks we figured 26 | that if we could just get a patch style output of the differences emailed to 27 | us, or provided in a ticket, then a developer could easily grok the changes 28 | and manually merge them back into the code base. 29 | 30 | And voila. This extension was built. 31 | 32 | Installation 33 | ------------ 34 | You can install the extension directly from the Chrom Web Store: 35 | 36 | https://chrome.google.com/webstore/detail/css-diff/bnkooidmjbobfbgncondmfoajbpafjio?hl=en 37 | 38 | Development Installation 39 | ~~~~~~~~~~~~~~~~~~~~~~~~ 40 | * Clone this repository 41 | * Open the Chrome extensions page by typing the following in your address 42 | bar:: 43 | 44 | chrome://extensions 45 | 46 | * Ensure Developer Mode is enable (top right) 47 | * Choose ``Load Unpacked Extension`` 48 | * Select the directory that was created when you cloned the repository 49 | 50 | Usage 51 | ----- 52 | Once the extension is installed just open and use the devtools window as you 53 | normally would. You'll notice that at the top is a new Tab that says ``CSS 54 | Diff``, when you visit it you'll be given the patch style output of your 55 | changes. 56 | 57 | Caveats 58 | ------- 59 | The initial & modified resources are tracked when the devtools panel is opened 60 | and this means that if you make modifications, close the devtools panel, and 61 | then re-open the devtools panel we'll consider the current state the "initial" 62 | resources. Once you start to make changes do not close the devtools panel or 63 | you will most likely need to reload the page and re-start all of your changes. 64 | 65 | This only works with uncompiled/unminified CSS sources because we're actually 66 | running a diff on the raw source in the inspected window. This shouldn't be a 67 | problem since this tool is meant for development purposes, but needed to be 68 | called out. 69 | 70 | 3rd party software 71 | ------------------ 72 | The CSS Diff extension contains the following software: 73 | 74 | * **jsdiff** 75 | https://github.com/kpdecker/jsdiff 76 | BSD license 77 | 78 | * **Mustache (Javascript)** 79 | https://github.com/janl/mustache.js 80 | MIT license 81 | 82 | Authors 83 | ------- 84 | The following fine individuals have contributed to this software: 85 | 86 | * @jondavey 87 | * @brentsmyth 88 | * @borgstrom 89 | -------------------------------------------------------------------------------- /assets/cssdiff-icon.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgstrom/cssdiff-chrome-extension/365fb1f4cdb260f2dcf7fc6afbf8892a87f666b0/assets/cssdiff-icon.ai -------------------------------------------------------------------------------- /devtools.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /devtools.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 FatBox Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | chrome.devtools.panels.create( 17 | 'CSS Diff', 18 | 'assets/cssdiff-icon.png', 19 | 'panel/index.html', 20 | function(panel) { 21 | var initial_resources = {}, 22 | modified_resources = {}; 23 | 24 | // collect our current resources 25 | chrome.devtools.inspectedWindow.getResources(function(resources) { 26 | for (var i = 0, c = resources.length; i < c; i++) { 27 | if (resources[i].type == 'stylesheet') { 28 | // use a self invoking function here to make sure the correct 29 | // instance of `resource` is used in the callback 30 | (function(resource) { 31 | resource.getContent(function(content, encoding) { 32 | initial_resources[resource.url] = content; 33 | }); 34 | })(resources[i]); 35 | } 36 | } 37 | }); 38 | 39 | // add a listener to track committed changes to resources 40 | chrome.devtools.inspectedWindow.onResourceContentCommitted.addListener(function(resource, content) { 41 | modified_resources[resource.url] = content; 42 | }); 43 | 44 | panel.onShown.addListener(function(window) { 45 | window.updateDiff(initial_resources, modified_resources); 46 | }); 47 | } 48 | ); 49 | -------------------------------------------------------------------------------- /icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgstrom/cssdiff-chrome-extension/365fb1f4cdb260f2dcf7fc6afbf8892a87f666b0/icon-128.png -------------------------------------------------------------------------------- /icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borgstrom/cssdiff-chrome-extension/365fb1f4cdb260f2dcf7fc6afbf8892a87f666b0/icon-48.png -------------------------------------------------------------------------------- /make-zip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm -f cssdiff.zip && zip cssdiff.zip -r panel/ manifest.json icon*.png devtools.* 3 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "CSS Diff", 4 | "version": "1.0", 5 | "author": "Evan Borgstrom", 6 | "description": "As you make CSS changes in the DevTools inspector window this will track your changes and provide you with a nicely formatted diff", 7 | "icons": { 8 | "48": "icon-48.png", 9 | "128": "icon-128.png" 10 | }, 11 | "devtools_page": "devtools.html" 12 | } 13 | -------------------------------------------------------------------------------- /panel/diff.js: -------------------------------------------------------------------------------- 1 | /* See LICENSE file for terms of use */ 2 | 3 | /* 4 | * Text diff implementation. 5 | * 6 | * This library supports the following APIS: 7 | * JsDiff.diffChars: Character by character diff 8 | * JsDiff.diffWords: Word (as defined by \b regex) diff which ignores whitespace 9 | * JsDiff.diffLines: Line based diff 10 | * 11 | * JsDiff.diffCss: Diff targeted at CSS content 12 | * 13 | * These methods are based on the implementation proposed in 14 | * "An O(ND) Difference Algorithm and its Variations" (Myers, 1986). 15 | * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.4.6927 16 | */ 17 | var JsDiff = (function() { 18 | /*jshint maxparams: 5*/ 19 | function clonePath(path) { 20 | return { newPos: path.newPos, components: path.components.slice(0) }; 21 | } 22 | function removeEmpty(array) { 23 | var ret = []; 24 | for (var i = 0; i < array.length; i++) { 25 | if (array[i]) { 26 | ret.push(array[i]); 27 | } 28 | } 29 | return ret; 30 | } 31 | function escapeHTML(s) { 32 | var n = s; 33 | n = n.replace(/&/g, '&'); 34 | n = n.replace(//g, '>'); 36 | n = n.replace(/"/g, '"'); 37 | 38 | return n; 39 | } 40 | 41 | var Diff = function(ignoreWhitespace) { 42 | this.ignoreWhitespace = ignoreWhitespace; 43 | }; 44 | Diff.prototype = { 45 | diff: function(oldString, newString) { 46 | // Handle the identity case (this is due to unrolling editLength == 0 47 | if (newString === oldString) { 48 | return [{ value: newString }]; 49 | } 50 | if (!newString) { 51 | return [{ value: oldString, removed: true }]; 52 | } 53 | if (!oldString) { 54 | return [{ value: newString, added: true }]; 55 | } 56 | 57 | newString = this.tokenize(newString); 58 | oldString = this.tokenize(oldString); 59 | 60 | var newLen = newString.length, oldLen = oldString.length; 61 | var maxEditLength = newLen + oldLen; 62 | var bestPath = [{ newPos: -1, components: [] }]; 63 | 64 | // Seed editLength = 0 65 | var oldPos = this.extractCommon(bestPath[0], newString, oldString, 0); 66 | if (bestPath[0].newPos+1 >= newLen && oldPos+1 >= oldLen) { 67 | return bestPath[0].components; 68 | } 69 | 70 | for (var editLength = 1; editLength <= maxEditLength; editLength++) { 71 | for (var diagonalPath = -1*editLength; diagonalPath <= editLength; diagonalPath+=2) { 72 | var basePath; 73 | var addPath = bestPath[diagonalPath-1], 74 | removePath = bestPath[diagonalPath+1]; 75 | oldPos = (removePath ? removePath.newPos : 0) - diagonalPath; 76 | if (addPath) { 77 | // No one else is going to attempt to use this value, clear it 78 | bestPath[diagonalPath-1] = undefined; 79 | } 80 | 81 | var canAdd = addPath && addPath.newPos+1 < newLen; 82 | var canRemove = removePath && 0 <= oldPos && oldPos < oldLen; 83 | if (!canAdd && !canRemove) { 84 | bestPath[diagonalPath] = undefined; 85 | continue; 86 | } 87 | 88 | // Select the diagonal that we want to branch from. We select the prior 89 | // path whose position in the new string is the farthest from the origin 90 | // and does not pass the bounds of the diff graph 91 | if (!canAdd || (canRemove && addPath.newPos < removePath.newPos)) { 92 | basePath = clonePath(removePath); 93 | this.pushComponent(basePath.components, oldString[oldPos], undefined, true); 94 | } else { 95 | basePath = clonePath(addPath); 96 | basePath.newPos++; 97 | this.pushComponent(basePath.components, newString[basePath.newPos], true, undefined); 98 | } 99 | 100 | var oldPos = this.extractCommon(basePath, newString, oldString, diagonalPath); 101 | 102 | if (basePath.newPos+1 >= newLen && oldPos+1 >= oldLen) { 103 | return basePath.components; 104 | } else { 105 | bestPath[diagonalPath] = basePath; 106 | } 107 | } 108 | } 109 | }, 110 | 111 | pushComponent: function(components, value, added, removed) { 112 | var last = components[components.length-1]; 113 | if (last && last.added === added && last.removed === removed) { 114 | // We need to clone here as the component clone operation is just 115 | // as shallow array clone 116 | components[components.length-1] = 117 | {value: this.join(last.value, value), added: added, removed: removed }; 118 | } else { 119 | components.push({value: value, added: added, removed: removed }); 120 | } 121 | }, 122 | extractCommon: function(basePath, newString, oldString, diagonalPath) { 123 | var newLen = newString.length, 124 | oldLen = oldString.length, 125 | newPos = basePath.newPos, 126 | oldPos = newPos - diagonalPath; 127 | while (newPos+1 < newLen && oldPos+1 < oldLen && this.equals(newString[newPos+1], oldString[oldPos+1])) { 128 | newPos++; 129 | oldPos++; 130 | 131 | this.pushComponent(basePath.components, newString[newPos], undefined, undefined); 132 | } 133 | basePath.newPos = newPos; 134 | return oldPos; 135 | }, 136 | 137 | equals: function(left, right) { 138 | var reWhitespace = /\S/; 139 | if (this.ignoreWhitespace && !reWhitespace.test(left) && !reWhitespace.test(right)) { 140 | return true; 141 | } else { 142 | return left === right; 143 | } 144 | }, 145 | join: function(left, right) { 146 | return left + right; 147 | }, 148 | tokenize: function(value) { 149 | return value; 150 | } 151 | }; 152 | 153 | var CharDiff = new Diff(); 154 | 155 | var WordDiff = new Diff(true); 156 | var WordWithSpaceDiff = new Diff(); 157 | WordDiff.tokenize = WordWithSpaceDiff.tokenize = function(value) { 158 | return removeEmpty(value.split(/(\s+|\b)/)); 159 | }; 160 | 161 | var CssDiff = new Diff(true); 162 | CssDiff.tokenize = function(value) { 163 | return removeEmpty(value.split(/([{}:;,]|\s+)/)); 164 | }; 165 | 166 | var LineDiff = new Diff(); 167 | LineDiff.tokenize = function(value) { 168 | return value.split(/^/m); 169 | }; 170 | 171 | return { 172 | Diff: Diff, 173 | 174 | diffChars: function(oldStr, newStr) { return CharDiff.diff(oldStr, newStr); }, 175 | diffWords: function(oldStr, newStr) { return WordDiff.diff(oldStr, newStr); }, 176 | diffWordsWithSpace: function(oldStr, newStr) { return WordWithSpaceDiff.diff(oldStr, newStr); }, 177 | diffLines: function(oldStr, newStr) { return LineDiff.diff(oldStr, newStr); }, 178 | 179 | diffCss: function(oldStr, newStr) { return CssDiff.diff(oldStr, newStr); }, 180 | 181 | createPatch: function(fileName, oldStr, newStr, oldHeader, newHeader) { 182 | var ret = []; 183 | 184 | ret.push('Index: ' + fileName); 185 | ret.push('==================================================================='); 186 | ret.push('--- ' + fileName + (typeof oldHeader === 'undefined' ? '' : '\t' + oldHeader)); 187 | ret.push('+++ ' + fileName + (typeof newHeader === 'undefined' ? '' : '\t' + newHeader)); 188 | 189 | var diff = LineDiff.diff(oldStr, newStr); 190 | if (!diff[diff.length-1].value) { 191 | diff.pop(); // Remove trailing newline add 192 | } 193 | diff.push({value: '', lines: []}); // Append an empty value to make cleanup easier 194 | 195 | function contextLines(lines) { 196 | return lines.map(function(entry) { return ' ' + entry; }); 197 | } 198 | function eofNL(curRange, i, current) { 199 | var last = diff[diff.length-2], 200 | isLast = i === diff.length-2, 201 | isLastOfType = i === diff.length-3 && (current.added !== last.added || current.removed !== last.removed); 202 | 203 | // Figure out if this is the last line for the given file and missing NL 204 | if (!/\n$/.test(current.value) && (isLast || isLastOfType)) { 205 | curRange.push('\\ No newline at end of file'); 206 | } 207 | } 208 | 209 | var oldRangeStart = 0, newRangeStart = 0, curRange = [], 210 | oldLine = 1, newLine = 1; 211 | for (var i = 0; i < diff.length; i++) { 212 | var current = diff[i], 213 | lines = current.lines || current.value.replace(/\n$/, '').split('\n'); 214 | current.lines = lines; 215 | 216 | if (current.added || current.removed) { 217 | if (!oldRangeStart) { 218 | var prev = diff[i-1]; 219 | oldRangeStart = oldLine; 220 | newRangeStart = newLine; 221 | 222 | if (prev) { 223 | curRange = contextLines(prev.lines.slice(-4)); 224 | oldRangeStart -= curRange.length; 225 | newRangeStart -= curRange.length; 226 | } 227 | } 228 | curRange.push.apply(curRange, lines.map(function(entry) { return (current.added?'+':'-') + entry; })); 229 | eofNL(curRange, i, current); 230 | 231 | if (current.added) { 232 | newLine += lines.length; 233 | } else { 234 | oldLine += lines.length; 235 | } 236 | } else { 237 | if (oldRangeStart) { 238 | // Close out any changes that have been output (or join overlapping) 239 | if (lines.length <= 8 && i < diff.length-2) { 240 | // Overlapping 241 | curRange.push.apply(curRange, contextLines(lines)); 242 | } else { 243 | // end the range and output 244 | var contextSize = Math.min(lines.length, 4); 245 | ret.push( 246 | '@@ -' + oldRangeStart + ',' + (oldLine-oldRangeStart+contextSize) 247 | + ' +' + newRangeStart + ',' + (newLine-newRangeStart+contextSize) 248 | + ' @@'); 249 | ret.push.apply(ret, curRange); 250 | ret.push.apply(ret, contextLines(lines.slice(0, contextSize))); 251 | if (lines.length <= 4) { 252 | eofNL(ret, i, current); 253 | } 254 | 255 | oldRangeStart = 0; newRangeStart = 0; curRange = []; 256 | } 257 | } 258 | oldLine += lines.length; 259 | newLine += lines.length; 260 | } 261 | } 262 | 263 | return ret.join('\n') + '\n'; 264 | }, 265 | 266 | applyPatch: function(oldStr, uniDiff) { 267 | var diffstr = uniDiff.split('\n'); 268 | var diff = []; 269 | var remEOFNL = false, 270 | addEOFNL = false; 271 | 272 | for (var i = (diffstr[0][0]==='I'?4:0); i < diffstr.length; i++) { 273 | if(diffstr[i][0] === '@') { 274 | var meh = diffstr[i].split(/@@ -(\d+),(\d+) \+(\d+),(\d+) @@/); 275 | diff.unshift({ 276 | start:meh[3], 277 | oldlength:meh[2], 278 | oldlines:[], 279 | newlength:meh[4], 280 | newlines:[] 281 | }); 282 | } else if(diffstr[i][0] === '+') { 283 | diff[0].newlines.push(diffstr[i].substr(1)); 284 | } else if(diffstr[i][0] === '-') { 285 | diff[0].oldlines.push(diffstr[i].substr(1)); 286 | } else if(diffstr[i][0] === ' ') { 287 | diff[0].newlines.push(diffstr[i].substr(1)); 288 | diff[0].oldlines.push(diffstr[i].substr(1)); 289 | } else if(diffstr[i][0] === '\\') { 290 | if (diffstr[i-1][0] === '+') { 291 | remEOFNL = true; 292 | } else if(diffstr[i-1][0] === '-') { 293 | addEOFNL = true; 294 | } 295 | } 296 | } 297 | 298 | var str = oldStr.split('\n'); 299 | for (var i = diff.length - 1; i >= 0; i--) { 300 | var d = diff[i]; 301 | for (var j = 0; j < d.oldlength; j++) { 302 | if(str[d.start-1+j] !== d.oldlines[j]) { 303 | return false; 304 | } 305 | } 306 | Array.prototype.splice.apply(str,[d.start-1,+d.oldlength].concat(d.newlines)); 307 | } 308 | 309 | if (remEOFNL) { 310 | while (!str[str.length-1]) { 311 | str.pop(); 312 | } 313 | } else if (addEOFNL) { 314 | str.push(''); 315 | } 316 | return str.join('\n'); 317 | }, 318 | 319 | convertChangesToXML: function(changes){ 320 | var ret = []; 321 | for ( var i = 0; i < changes.length; i++) { 322 | var change = changes[i]; 323 | if (change.added) { 324 | ret.push(''); 325 | } else if (change.removed) { 326 | ret.push('