├── .gitignore ├── LICENSE ├── README.md ├── bookmarklet.js ├── gulpfile.js ├── index.js ├── index.min.js ├── karma.conf.js ├── package.json ├── src ├── auditStyleGuide.js ├── auditView.js ├── bookmarkletCore.js ├── core.js ├── parseStyleSheets.js └── run.js └── test ├── auditStyleGuide.spec.js ├── core.spec.js ├── css ├── overrideStyles.css └── patternLib.css ├── js └── jquery-2.1.3.js └── parseStyleSheets.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | index.test.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Steven Lambert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Style Guide Auditing Tool 2 | 3 | Audit the CSS on a page to see what elements are using styles from the style guide and which styles are overriding them. 4 | 5 | **IMPORTANT: Dropbox changed how their Public folder worked and removed all previous links to files hosted in it (which is where the Bookmarklet script is kept). All previous versions (1.0.1 and below) of the bookmarklet will no longer work until you've updated to the latest code.** 6 | 7 | ## How it works 8 | 9 | The code parses all the style sheets on the page and keeps track of every rule and how it affects all elements. When you run the audit, it takes this information and looks at which rules from other style sheets are overriding the rules from the style guide. Any elements that have an override are highlighted. 10 | 11 | If you click on any highlighted element, the tool will show you which rules from which style sheets are providing the overrides. 12 | 13 | The tool will also outline each element that is using a rule from the style guide. This helps to highlight any elements that should be using the style guide but aren't. Rules that purely use element selectors (no class selector in the rule) will be ignored since those are always applied so long as the style guide is being used on the page. 14 | 15 | ![](/../screenshots/image0.png?raw=true) 16 | 17 | ## How to use 18 | 19 | Out of the box, the tool provides two global functions: `parseStyleSheets()` and `auditStyleGuide()`. Running `parseStyleSheets()` will parse the style sheets on the page. After the function completes, you can run `auditStyleGuide()` to run the audit. 20 | 21 | `auditStyleGuide()` takes two parameters, the first is a string that is the name of the style guide to audit. It uses fuzzy matching to find the name for cases when your compiled style sheet uses hashes, or if you use a minified version on production and a non-minified version on localhost. 22 | 23 | ```javascript 24 | // matches any styleSheet that contains the text 'pattern-lib': 25 | // localhost/css/pattern-lib.css 26 | // http://myDomain/styles/pattern-lib.min.css 27 | // http://myDomain/styles/pattern-lib-17D8401NDL.css, 28 | auditStyleGuide('pattern-lib'); 29 | ``` 30 | 31 | The second parameter is an array of style sheets to ignore from the audit. This is useful if you are using a global style sheet like Bootstrap and don't care to know if it overrides any of your style guide's rules. Again, it uses fuzzy matching for the name. 32 | 33 | ```javascript 34 | // ignores any styleSheet that contains the text 'bootstrap' or 'normalize' 35 | auditStyleGuide('pattern-lib', ['bootstrap','normalize']); 36 | ``` 37 | 38 | ## Customizing the audit 39 | 40 | The tool also allows you to customize the audit. You can pass an array of custom rules to `auditStyleGuide()` as the third parameter that will highlight any element that fails the rule. 41 | 42 | For example, let's say you wanted to validate the accessibility of your page by ensuring that all anchor tags navigate and are not just there for [JavaScript events](http://webaim.org/techniques/hypertext/). You could write a rule that checks that all anchor tags have a valid `href` property and run the rule with the audit. Any anchor tags that do not have a valid `href` will be highlighted. 43 | 44 | ```javascript 45 | var customRules = [{ 46 | type: 'my-custom-rule', 47 | selector: 'a[href^="javascript"], a[href="#"], a:not([href])', 48 | description: 'Anchor tags that do not navigate should be buttons.' 49 | }]; 50 | 51 | auditStyleGuide('pattern-lib', ['bootstrap'], customRules); 52 | ``` 53 | 54 | A rule consists of three properties: 55 | - `type`: a slug that identifies the rule. This slug will be added to the `data-style-audit` attribute of any element that fails the rule. This is useful if you want to give those elements your own styling. 56 | - `selector`: how to identify any elements that do not pass the rule (using `document.querySelectorAll`) 57 | - `description`: the text to display in the audit results for any element that fails the rule. 58 | 59 | ## Using the bookmarklet 60 | 61 | You can also create a bookmarklet that will run the entire audit for you on any page. Just edit `src/run.js` to include any custom rules and the style sheet to audit, and then run `gulp scripts`. This will create `bookmarklet.js` that you can then copy into your favorites bar. 62 | 63 | This is an excellent tool to give to designers who can then help run the audits of the site on their own. 64 | 65 | **IMPORTANT** - If you are going to create your own custom bookmarklet, please create a branch on your fork where you will push the changes. I will not accept any pull requests that change `src/run.js` or `bookmarklet.js`, so if you need to submit a fix, you must do so on a clean branch without custom bookmarklet changes. [More info](http://stackoverflow.com/questions/10100933/how-to-ignore-files-and-folders-with-pull-requests-to-github-in-distinct-git-clo). 66 | -------------------------------------------------------------------------------- /bookmarklet.js: -------------------------------------------------------------------------------- 1 | javascript:(function(e){"use strict";var t=e.createElement("script");t.onload=function(){var t=e.querySelector(".audit-push-results");t.removeAttribute("style"),t.firstChild.removeAttribute("style"),parseStyleSheets()},t.src="https://www.dropbox.com/s/vs4ny7igudc23jb/index-1.0.1.min.js?dl=0&raw=1",e.head.appendChild(t),e.addEventListener("styleSheetsParsed",function(){})})(document); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var jshint = require('gulp-jshint'); 3 | var concat = require('gulp-concat-util'); 4 | var uglify = require('gulp-uglify'); 5 | var rename = require('gulp-rename'); 6 | var size = require('gulp-size'); 7 | var karma = require('karma').server; 8 | var autoprefixer = require('gulp-autoprefixer'); 9 | 10 | function swallowError(error) { 11 | this.emit('end'); 12 | } 13 | 14 | gulp.task('lint', function() { 15 | return gulp.src('src/*.js') 16 | .pipe(jshint()) 17 | .pipe(jshint.reporter( 'jshint-stylish' )); 18 | }); 19 | 20 | gulp.task('test', function(done) { 21 | karma.start({ 22 | configFile: __dirname + '/karma.conf.js' 23 | }, done); 24 | }); 25 | 26 | gulp.task('scripts', function() { 27 | // auto-prefix test css files 28 | gulp.src('test/css/*.css') 29 | .pipe(autoprefixer()) 30 | .pipe(gulp.dest('test/css')); 31 | 32 | // index.test.js is used in the test code to have access to all private function/variables 33 | // index.js is the unminified output 34 | // index.min.js is what's hosted on Dropbox and is used by the bookmarklet script 35 | gulp.src(['src/core.js', 'node_modules/specificity/specificity.js', 'src/*.js', '!src/run.js', '!src/bookmarkletCore.js']) 36 | .pipe(concat('index.test.js')) 37 | .pipe(gulp.dest('.')) 38 | .pipe(rename('index.js')) 39 | .pipe(concat.header('(function(window, document) {\n\'use strict\';\n')) 40 | .pipe(concat.footer('\n\n})(window, document);')) 41 | .pipe(size()) 42 | .pipe(gulp.dest('.')) 43 | .pipe(rename('index.min.js')) 44 | // prevent IIFE from starting with !, which breaks bookmarklet return value 45 | .pipe(uglify({compress: {negate_iife: false}})) 46 | .pipe(size()) 47 | .pipe(gulp.dest('.')) 48 | 49 | // create the bookmarklet code 50 | return gulp.src(['src/bookmarkletCore.js', 'src/run.js']) 51 | .pipe(concat('bookmarklet.js')) 52 | .pipe(concat.header('(function(document) {\n\'use strict\';\n')) 53 | .pipe(concat.footer('\n\n})(document);')) 54 | // prevent IIFE from starting with !, which breaks bookmarklet return value 55 | .pipe(uglify({compress: {negate_iife: false}})) 56 | .pipe(concat.header('javascript:')) 57 | .pipe(gulp.dest('.')); 58 | 59 | // return; 60 | 61 | // index.run.js includes the run script for running the code 62 | // return gulp.src(['src/core.js', 'node_modules/specificity/specificity.js', 'src/*.js']) 63 | // .pipe(concat('index.js')) 64 | 65 | }); 66 | 67 | gulp.task('watch', function() { 68 | gulp.watch('src/*.js', ['lint', 'scripts']); 69 | }); 70 | 71 | gulp.task('default', ['lint', 'scripts', 'test', 'watch']); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | (function(window, document) { 2 | 'use strict'; 3 | /*jshint unused:false */ 4 | /*jshint latedef: nofunc */ 5 | /* global console */ 6 | 7 | var trayHeight = 300; 8 | 9 | // create a div that will push the content out of the way of the results tray 10 | var push = document.createElement('div'); 11 | push.classList.add('audit-push-results'); 12 | push.innerHTML = '
'; 13 | document.body.appendChild(push); 14 | 15 | // append the tray to body 16 | var auditTool = document.createElement('div'); 17 | auditTool.setAttribute('class', 'audit-results'); 18 | document.body.appendChild(auditTool); 19 | preventParentScroll(auditTool); 20 | 21 | // create a title for the tray 22 | var code = document.createElement('code'); 23 | code.setAttribute('class', 'language-markup'); 24 | var pre = document.createElement('pre'); 25 | pre.appendChild(code); 26 | 27 | var title = document.createElement('div'); 28 | title.setAttribute('class', 'audit-results__title'); 29 | title.appendChild(pre); 30 | auditTool.appendChild(title); 31 | 32 | // create a container for the results 33 | var container = document.createElement('div'); 34 | container.setAttribute('class', 'audit-results__body'); 35 | auditTool.appendChild(container); 36 | 37 | // append a styles for the tray to body 38 | var trayStyle = document.createElement('style'); 39 | trayStyle.setAttribute('data-style-skip', 'true'); 40 | var trayCss = '' + 41 | '.audit-results {' + 42 | 'position: fixed;' + 43 | 'bottom: -' + trayHeight + 'px;' + 44 | 'left: 0;' + 45 | 'right: 0;' + 46 | 'height: ' + trayHeight + 'px;' + 47 | 'background: white;' + 48 | 'border-top: 0 solid black;' + 49 | 'transition: bottom 300ms, border 300ms;' + 50 | 'overflow-y: auto;' + 51 | 'z-index: 1000000' + 52 | '}' + 53 | 'body.open-audit .audit-results {' + 54 | 'bottom: 0;' + 55 | 'border-top-width: 1px;' + 56 | '}' + 57 | '.audit-push-results {' + 58 | 'height: 0;' + 59 | 'transition: height 300ms;' + 60 | '}' + 61 | '.audit-push-results[data-loading] {' + 62 | 'position: fixed;' + 63 | 'left: 0;' + 64 | 'right: 0;' + 65 | 'top: 0;' + 66 | 'bottom: 0;' + 67 | 'background: rgba(166,166,166,.6);' + 68 | 'height: auto;' + 69 | 'z-index: 10000000;' + 70 | 'height: auto !important' + 71 | '}' + 72 | '.audit-push-results[data-loading] div {' + 73 | 'background-color: #fff;' + 74 | 'border-radius: 100%;' + 75 | 'margin: 2px;' + 76 | '-webkit-animation-fill-mode: both;' + 77 | 'animation-fill-mode: both;' + 78 | 'border: 3px solid #fff;' + 79 | 'border-bottom-color: transparent;' + 80 | 'height: 100px;' + 81 | 'width: 100px;' + 82 | 'background: transparent !important;' + 83 | '-webkit-animation: styleRotate 0.75s 0s linear infinite;' + 84 | 'animation: styleRotate 0.75s 0s linear infinite;' + 85 | 'position: absolute;' + 86 | 'top: 50%;' + 87 | 'left: 50%;' + 88 | 'margin-left: -50px;' + 89 | 'margin-top: -50px;' + 90 | '}' + 91 | 'body.open-audit .audit-push-results {' + 92 | 'height: ' + trayHeight + 'px;' + 93 | '}' + 94 | '.audit-results__body {' + 95 | 'padding: 1em;' + 96 | '}' + 97 | // TODO: add back when I have a way to ignore results 98 | // '.audit-results__body ul {' + 99 | // 'margin: 0;' + 100 | // '}' + 101 | '.audit-results__body li {' + 102 | 'margin-bottom: 10px;' + 103 | // TODO: add back when I have a way to ignore results 104 | // 'display: table;' + 105 | '}' + 106 | // TODO: add back when I have a way to ignore results 107 | // '.audit-results__body div {' + 108 | // 'display: table-cell;' + 109 | // '}' + 110 | '.audit-results__body div:first-child {' + // TODO: remove when I have a way to ignore results 111 | 'display: none;' + 112 | '}' + 113 | '.audit-results__body input[type="checkbox"] {' + 114 | 'float: none;' + 115 | 'margin: 0;' + 116 | 'padding: 0;' + 117 | '}' + 118 | '.audit-results__body label {' + 119 | 'font-size: 16px;' + 120 | 'padding-left: 0;' + // TODO: change back to 10px when I have a way to ignore results 121 | '}' + 122 | '.audit-results__body code {' + 123 | 'margin-bottom: 4px;' + 124 | 'display: inline-block;' + 125 | '}' + 126 | // override bootstrap and prism styles 127 | '.audit-results pre[class*=language-] {' + 128 | 'border-radius: 0;' + 129 | 'margin: 0;' + 130 | '}' + 131 | '.audit-results pre[class*=language-]>code[data-language]::before {' + 132 | 'display: none;' + 133 | '}' + 134 | // make all audit elements a different color 135 | '[data-style-audit] {' + 136 | 'background: salmon !important;' + 137 | 'cursor: pointer !important;' + 138 | '}' + 139 | // make the border of all elements using the style a different color 140 | '[data-style-using] {' + 141 | 'outline: 1px dashed midnightblue !important' + 142 | '}' + 143 | // rotate animation 144 | '@keyframes styleRotate {' + 145 | '0%, {' + 146 | '-webkit-transform: rotate(0deg);' + 147 | 'transform: rotate(0deg);' + 148 | '}' + 149 | '50% {' + 150 | '-webkit-transform: rotate(180deg);' + 151 | 'transform: rotate(180deg);' + 152 | '}' + 153 | '100% {' + 154 | '-webkit-transform: rotate(360deg);' + 155 | 'transform: rotate(360deg);' + 156 | '}' + 157 | '}' + 158 | '@-moz-keyframes styleRotate {' + 159 | '0% {' + 160 | '-webkit-transform: rotate(0deg);' + 161 | 'transform: rotate(0deg);' + 162 | '}' + 163 | '50% {' + 164 | '-webkit-transform: rotate(180deg);' + 165 | 'transform: rotate(180deg);' + 166 | '}' + 167 | '100% {' + 168 | '-webkit-transform: rotate(360deg);' + 169 | 'transform: rotate(360deg);' + 170 | '}' + 171 | '}' + 172 | '@-webkit-keyframes styleRotate {' + 173 | '0% {' + 174 | '-webkit-transform: rotate(0deg);' + 175 | 'transform: rotate(0deg);' + 176 | '}' + 177 | '50% {' + 178 | '-webkit-transform: rotate(180deg);' + 179 | 'transform: rotate(180deg);' + 180 | '}' + 181 | '100% {' + 182 | '-webkit-transform: rotate(360deg);' + 183 | 'transform: rotate(360deg);' + 184 | '}' + 185 | '}' + 186 | '@-o-keyframes styleRotate {' + 187 | '0% {' + 188 | '-webkit-transform: rotate(0deg);' + 189 | 'transform: rotate(0deg);' + 190 | '}' + 191 | '50% {' + 192 | '-webkit-transform: rotate(180deg);' + 193 | 'transform: rotate(180deg);' + 194 | '}' + 195 | '100% {' + 196 | '-webkit-transform: rotate(360deg);' + 197 | 'transform: rotate(360deg);' + 198 | '}' + 199 | '}' + 200 | '@-ms-keyframes styleRotate {' + 201 | '0% {' + 202 | '-webkit-transform: rotate(0deg);' + 203 | 'transform: rotate(0deg);' + 204 | '}' + 205 | '50% {' + 206 | '-webkit-transform: rotate(180deg);' + 207 | 'transform: rotate(180deg);' + 208 | '}' + 209 | '100% {' + 210 | '-webkit-transform: rotate(360deg);' + 211 | 'transform: rotate(360deg);' + 212 | '}' + 213 | '}'; 214 | trayStyle.appendChild(document.createTextNode(trayCss)); 215 | document.head.appendChild(trayStyle); 216 | 217 | // load prism.js syntax highlighting 218 | if (!window.Prism) { 219 | var prismJS = document.createElement('script'); 220 | prismJS.setAttribute('async', true); 221 | prismJS.src = 'https://cdnjs.cloudflare.com/ajax/libs/prism/0.0.1/prism.js'; 222 | document.body.appendChild(prismJS); 223 | var prismCSS = document.createElement('link'); 224 | prismCSS.setAttribute('rel', 'stylesheet'); 225 | prismCSS.href = 'https://cdnjs.cloudflare.com/ajax/libs/prism/0.0.1/prism.min.css'; 226 | prismCSS.setAttribute('data-style-skip', 'true'); 227 | document.head.appendChild(prismCSS); 228 | } 229 | 230 | /** 231 | * Load a styleSheet from a cross domain URL. 232 | * @param {string} url - The URL of the styleSheet to load. 233 | * @see http://stackoverflow.com/questions/3211536/accessing-cross-domain-style-sheet-with-cssrules 234 | */ 235 | function loadCSSCors(url, callback) { 236 | var XHR = XMLHttpRequest; 237 | var xhr; 238 | var hasCred = false; 239 | try { 240 | hasCred = XMLHttpRequest && ('withCredentials' in (xhr = new XHR())); 241 | } catch(e) {} 242 | 243 | if (!hasCred) { 244 | console.error('CORS not supported'); 245 | return; 246 | } 247 | 248 | xhr.open('GET', url); 249 | xhr.onload = function() { 250 | xhr.onload = xhr.onerror = null; 251 | if (xhr.status < 200 || xhr.status >=300) { 252 | console.error('style failed to load: ' + url); 253 | } 254 | else { 255 | var styleTag = document.createElement('style'); 256 | styleTag.appendChild(document.createTextNode(xhr.responseText)); 257 | styleTag.setAttribute('data-url', url); // set url for testing 258 | document.head.appendChild(styleTag); 259 | callback(styleTag); 260 | 261 | // clean up style tag when callback is finished 262 | styleTag.remove(); 263 | } 264 | }; 265 | xhr.onerror = function() { 266 | xhr.onload = xhr.onerror = null; 267 | console.error('XHR CORS CSS fail:' + url); 268 | }; 269 | xhr.send(); 270 | } 271 | 272 | /** 273 | * Wrapper function for getting a styleSheets rules 274 | * @param {CSSStyleSheet} sheet - The styleSheet to get the rules from. 275 | * @return {CSSRuleList} 276 | */ 277 | function getRules(sheet) { 278 | try { 279 | return sheet.cssRules || sheet.rules; 280 | } 281 | catch (e) { 282 | // Firefox will throw an insecure error when trying to look at the rules of a 283 | // cross domain styleSheet. We'll just eat the error and continue as the 284 | // code will automatically request the styleSheet through CORS to be able 285 | // to read it 286 | return; 287 | } 288 | } 289 | 290 | /** 291 | * Get a styleSheets rules object, taking into account styleSheets that are hosted on 292 | * different domains. 293 | * @param {CSSStyleSheet} sheet - The styleSheet to get the rules from. 294 | * @param {function} callback - Callback function to be called (needed for xhr CORS request) 295 | */ 296 | var styleSheets = {}; // keep a list of already requested styleSheets so we don't have to request them again 297 | function getStyleSheetRules(sheet, callback) { 298 | // skip any styleSheets we don't want to parse (e.g. prism.css, audit styles) 299 | if (sheet.ownerNode && sheet.ownerNode.hasAttribute('data-style-skip')) { 300 | callback([], sheet.href); 301 | return; 302 | } 303 | 304 | var rules = getRules(sheet); 305 | 306 | // check to see if we've already loaded this styleSheet 307 | if (!rules && styleSheets[sheet.href]) { 308 | rules = styleSheets[sheet.href].rules; 309 | 310 | callback(rules, sheet.href); 311 | } 312 | // this is an external styleSheet so we need to request it through CORS 313 | else if (!rules) { 314 | (function (sheet) { 315 | loadCSSCors(sheet.href, function(corsSheet) { 316 | styleSheets[sheet.href] = {}; 317 | styleSheets[sheet.href].styleSheet = corsSheet.sheet; 318 | styleSheets[sheet.href].rules = getRules(corsSheet.sheet); 319 | 320 | callback(styleSheets[sheet.href].rules, sheet.href); 321 | }); 322 | })(sheet); 323 | } 324 | else { 325 | callback(rules, sheet.href); 326 | } 327 | } 328 | 329 | /** 330 | * Iterate over a list of CSS rules and return only valid rules (e.g. no keyframe or 331 | * font-family declarations). 332 | * @param {CSSRuleList} rules - CSS rules to parse. 333 | * @see http://toddmotto.com/ditch-the-array-foreach-call-nodelist-hack/ 334 | */ 335 | function forEachRule(rules, callback, scope) { 336 | var rule; 337 | 338 | for (var i = 0, len = rules.length; i < len; i++) { 339 | rule = rules[i]; 340 | 341 | // keyframe and font-family declarations do not have selectorText 342 | if (!rule.selectorText) { 343 | continue; 344 | } 345 | 346 | callback.call(scope, rule, i); 347 | } 348 | } 349 | 350 | /** 351 | * Prevents a child element from scrolling a parent element (aka document). 352 | * @param {Element} element - Scrolling element. 353 | * @see http://codepen.io/Merri/pen/nhijD/ 354 | */ 355 | function preventParentScroll(element) { 356 | var html = document.getElementsByTagName('html')[0], 357 | htmlTop = 0, 358 | htmlBlockScroll = 0, 359 | minDeltaY, 360 | // this is where you put all your logic 361 | wheelHandler = function (e) { 362 | // do not prevent scrolling if element can't scroll 363 | if (element.scrollHeight <= element.clientHeight) { 364 | return; 365 | } 366 | 367 | // normalize Y delta 368 | if (minDeltaY > Math.abs(e.deltaY) || !minDeltaY) { 369 | minDeltaY = Math.abs(e.deltaY); 370 | } 371 | 372 | // prevent other wheel events and bubbling in general 373 | if(e.stopPropagation) { 374 | e.stopPropagation(); 375 | } else { 376 | e.cancelBubble = true; 377 | } 378 | 379 | // most often you want to prevent default scrolling behavior (full page scroll!) 380 | if( (e.deltaY < 0 && element.scrollTop === 0) || (e.deltaY > 0 && element.scrollHeight === element.scrollTop + element.clientHeight) ) { 381 | if(e.preventDefault) { 382 | e.preventDefault(); 383 | } else { 384 | e.returnValue = false; 385 | } 386 | } else { 387 | // safeguard against fast scroll in IE and mac 388 | if(!htmlBlockScroll) { 389 | htmlTop = html.scrollTop; 390 | } 391 | htmlBlockScroll++; 392 | // even IE11 updates scrollTop after the wheel event :/ 393 | setTimeout(function() { 394 | htmlBlockScroll--; 395 | if(!htmlBlockScroll && html.scrollTop !== htmlTop) { 396 | html.scrollTop = htmlTop; 397 | } 398 | }, 0); 399 | } 400 | }, 401 | // here we do only compatibility stuff 402 | mousewheelCompatibility = function (e) { 403 | // no need to convert more than this, we normalize the value anyway 404 | e.deltaY = -e.wheelDelta; 405 | // and then call our main handler 406 | wheelHandler(e); 407 | }; 408 | 409 | // do not add twice! 410 | if(element.removeWheelListener) { 411 | return; 412 | } 413 | 414 | if (element.addEventListener) { 415 | element.addEventListener('wheel', wheelHandler, false); 416 | element.addEventListener('mousewheel', mousewheelCompatibility, false); 417 | // expose a remove method 418 | element.removeWheelListener = function() { 419 | element.removeEventListener('wheel', wheelHandler, false); 420 | element.removeEventListener('mousewheel', mousewheelCompatibility, false); 421 | element.removeWheelListener = undefined; 422 | }; 423 | } 424 | } 425 | 426 | /** 427 | * Convert rgb values from the stylesheet to hex. 428 | * @param {number} r - Red value. 429 | * @param {number} g - Green value. 430 | * @param {number} b - Blue value. 431 | * @returns {string} 432 | * @see http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb 433 | */ 434 | function rgbToHex(r, g, b) { 435 | return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); 436 | } 437 | 438 | /** 439 | * Convert a hyphen-separated string into a camel case string. 440 | * @param {string} str - Hyphen-separated string. 441 | * @returns {string} 442 | */ 443 | function camelCase(str) { 444 | str = str.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); }); 445 | 446 | // webkit is lowercase in Chrome 447 | if (str.indexOf('Webkit') === 0) { 448 | str = str[0].toLowerCase() + str.slice(1); 449 | } 450 | 451 | return str; 452 | } 453 | 454 | // translate browser specific styles to their actual style. 455 | // @see https://gist.github.com/dennisroethig/7078659 456 | var propertyMap = { 457 | 'float': 'cssFloat', 458 | 'margin-left-value': 'marginLeft', 459 | 'margin-left-ltr-source': '', 460 | 'margin-left-rtl-source': '', 461 | 'margin-right-value': 'marginRight', 462 | 'margin-right-ltr-source': '', 463 | 'margin-right-rtl-source': '', 464 | 'padding-right-value': 'paddingRight', 465 | 'padding-right-ltr-source': '', 466 | 'padding-right-rtl-source': '', 467 | 'padding-left-value': 'paddingLeft', 468 | 'padding-left-ltr-source': '', 469 | 'padding-left-rtl-source': '' 470 | }; 471 | 472 | /** 473 | * Get a value from a style rule. 474 | * @param {CSS2Properties} style - CSS Style property object. 475 | * @param {string} property - CSS property name. 476 | */ 477 | function getStyleValue(style, property) { 478 | // Chrome maps hyphen-separated names to their camel case name 479 | // Firefox uses camel case names 480 | // if no value is found, default to "" so that .indexOf will still work 481 | var value = style[property] || style[ camelCase(property) ] || 482 | style[ propertyMap[property] ] || ''; 483 | 484 | return value; 485 | } 486 | /** 487 | * Calculates the specificity of CSS selectors 488 | * http://www.w3.org/TR/css3-selectors/#specificity 489 | * 490 | * Returns an array of objects with the following properties: 491 | * - selector: the input 492 | * - specificity: e.g. 0,1,0,0 493 | * - parts: array with details about each part of the selector that counts towards the specificity 494 | */ 495 | var SPECIFICITY = (function() { 496 | var calculate, 497 | calculateSingle; 498 | 499 | calculate = function(input) { 500 | var selectors, 501 | selector, 502 | i, 503 | len, 504 | results = []; 505 | 506 | // Separate input by commas 507 | selectors = input.split(','); 508 | 509 | for (i = 0, len = selectors.length; i < len; i += 1) { 510 | selector = selectors[i]; 511 | if (selector.length > 0) { 512 | results.push(calculateSingle(selector)); 513 | } 514 | } 515 | 516 | return results; 517 | }; 518 | 519 | // Calculate the specificity for a selector by dividing it into simple selectors and counting them 520 | calculateSingle = function(input) { 521 | var selector = input, 522 | findMatch, 523 | typeCount = { 524 | 'a': 0, 525 | 'b': 0, 526 | 'c': 0 527 | }, 528 | parts = [], 529 | // The following regular expressions assume that selectors matching the preceding regular expressions have been removed 530 | attributeRegex = /(\[[^\]]+\])/g, 531 | idRegex = /(#[^\s\+>~\.\[:]+)/g, 532 | classRegex = /(\.[^\s\+>~\.\[:]+)/g, 533 | pseudoElementRegex = /(::[^\s\+>~\.\[:]+|:first-line|:first-letter|:before|:after)/gi, 534 | // A regex for pseudo classes with brackets - :nth-child(), :nth-last-child(), :nth-of-type(), :nth-last-type(), :lang() 535 | pseudoClassWithBracketsRegex = /(:[\w-]+\([^\)]*\))/gi, 536 | // A regex for other pseudo classes, which don't have brackets 537 | pseudoClassRegex = /(:[^\s\+>~\.\[:]+)/g, 538 | elementRegex = /([^\s\+>~\.\[:]+)/g; 539 | 540 | // Find matches for a regular expression in a string and push their details to parts 541 | // Type is "a" for IDs, "b" for classes, attributes and pseudo-classes and "c" for elements and pseudo-elements 542 | findMatch = function(regex, type) { 543 | var matches, i, len, match, index, length; 544 | if (regex.test(selector)) { 545 | matches = selector.match(regex); 546 | for (i = 0, len = matches.length; i < len; i += 1) { 547 | typeCount[type] += 1; 548 | match = matches[i]; 549 | index = selector.indexOf(match); 550 | length = match.length; 551 | parts.push({ 552 | selector: input.substr(index, length), 553 | type: type, 554 | index: index, 555 | length: length 556 | }); 557 | // Replace this simple selector with whitespace so it won't be counted in further simple selectors 558 | selector = selector.replace(match, Array(length + 1).join(' ')); 559 | } 560 | } 561 | }; 562 | 563 | // Replace escaped characters with plain text, using the "A" character 564 | // https://www.w3.org/TR/CSS21/syndata.html#characters 565 | (function() { 566 | var replaceWithPlainText = function(regex) { 567 | var matches, i, len, match; 568 | if (regex.test(selector)) { 569 | matches = selector.match(regex); 570 | for (i = 0, len = matches.length; i < len; i += 1) { 571 | match = matches[i]; 572 | selector = selector.replace(match, Array(match.length + 1).join('A')); 573 | } 574 | } 575 | }, 576 | // Matches a backslash followed by six hexadecimal digits followed by an optional single whitespace character 577 | escapeHexadecimalRegex = /\\[0-9A-Fa-f]{6}\s?/g, 578 | // Matches a backslash followed by fewer than six hexadecimal digits followed by a mandatory single whitespace character 579 | escapeHexadecimalRegex2 = /\\[0-9A-Fa-f]{1,5}\s/g, 580 | // Matches a backslash followed by any character 581 | escapeSpecialCharacter = /\\./g; 582 | 583 | replaceWithPlainText(escapeHexadecimalRegex); 584 | replaceWithPlainText(escapeHexadecimalRegex2); 585 | replaceWithPlainText(escapeSpecialCharacter); 586 | }()); 587 | 588 | // Remove the negation psuedo-class (:not) but leave its argument because specificity is calculated on its argument 589 | (function() { 590 | var regex = /:not\(([^\)]*)\)/g; 591 | if (regex.test(selector)) { 592 | selector = selector.replace(regex, ' $1 '); 593 | } 594 | }()); 595 | 596 | // Remove anything after a left brace in case a user has pasted in a rule, not just a selector 597 | (function() { 598 | var regex = /{[^]*/gm, 599 | matches, i, len, match; 600 | if (regex.test(selector)) { 601 | matches = selector.match(regex); 602 | for (i = 0, len = matches.length; i < len; i += 1) { 603 | match = matches[i]; 604 | selector = selector.replace(match, Array(match.length + 1).join(' ')); 605 | } 606 | } 607 | }()); 608 | 609 | // Add attribute selectors to parts collection (type b) 610 | findMatch(attributeRegex, 'b'); 611 | 612 | // Add ID selectors to parts collection (type a) 613 | findMatch(idRegex, 'a'); 614 | 615 | // Add class selectors to parts collection (type b) 616 | findMatch(classRegex, 'b'); 617 | 618 | // Add pseudo-element selectors to parts collection (type c) 619 | findMatch(pseudoElementRegex, 'c'); 620 | 621 | // Add pseudo-class selectors to parts collection (type b) 622 | findMatch(pseudoClassWithBracketsRegex, 'b'); 623 | findMatch(pseudoClassRegex, 'b'); 624 | 625 | // Remove universal selector and separator characters 626 | selector = selector.replace(/[\*\s\+>~]/g, ' '); 627 | 628 | // Remove any stray dots or hashes which aren't attached to words 629 | // These may be present if the user is live-editing this selector 630 | selector = selector.replace(/[#\.]/g, ' '); 631 | 632 | // The only things left should be element selectors (type c) 633 | findMatch(elementRegex, 'c'); 634 | 635 | // Order the parts in the order they appear in the original selector 636 | // This is neater for external apps to deal with 637 | parts.sort(function(a, b) { 638 | return a.index - b.index; 639 | }); 640 | 641 | return { 642 | selector: input, 643 | specificity: '0,' + typeCount.a.toString() + ',' + typeCount.b.toString() + ',' + typeCount.c.toString(), 644 | parts: parts 645 | }; 646 | }; 647 | 648 | return { 649 | calculate: calculate 650 | }; 651 | }()); 652 | 653 | // Export for Node JS 654 | if (typeof exports !== 'undefined') { 655 | exports.calculate = SPECIFICITY.calculate; 656 | } 657 | 658 | /*jshint -W083 */ 659 | /*jshint -W084 */ 660 | /*jshint unused:false */ 661 | /* global console, getStyleSheetRules, forEachRule, rgbToHex, push, getStyleValue, compareSpecificity, SPECIFICITY */ 662 | 663 | var audit = {elms: []}; 664 | var rgbValues = /([0-9]){1,3}/g; 665 | 666 | /** 667 | * Produce a JSON audit. 668 | * @param {string|string[]} styleGuideSheet - href substring that uniquely identifies the style guide styleSheet(s) 669 | * @param {string|string[]} ignoreSheet - href substring that uniquely identifies any styleSheets that should be ignored in the audit 670 | * @param {object[]} customRules - Custom rules to be audited. 671 | * @example 672 | * // references any styleSheet that contains the text 'pattern-lib' 673 | * // e.g. localhost/css/pattern-lib.css 674 | * // e.g. http://myDomain/styles/pattern-lib-17D8401NDL.css 675 | * auditStyleGuide('pattern-lib'); 676 | */ 677 | function auditStyleGuide(styleGuideSheet, ignoreSheet, customRules) { 678 | var link, sheet, elm, elms, el, selectors, selector, specificity, property, value, elStyle, ignore; 679 | 680 | if (!Array.isArray(styleGuideSheet)) { 681 | styleGuideSheet = [styleGuideSheet]; 682 | } 683 | 684 | if (!Array.isArray(ignoreSheet)) { 685 | ignoreSheet = [ignoreSheet]; 686 | } 687 | 688 | customRules = customRules || []; 689 | 690 | // reset previous audit 691 | for (var x = 0; elm = audit.elms[x]; x++) { 692 | elm.problems = []; 693 | } 694 | audit = {elms: []}; 695 | 696 | elms = document.body.querySelectorAll('[data-style-audit]'); 697 | for (x = 0; elm = elms[x]; x++) { 698 | elm.removeAttribute('data-style-audit'); 699 | } 700 | 701 | elms = document.body.querySelectorAll('[data-style-using]'); 702 | for (x = 0; elm = elms[x]; x++) { 703 | elm.removeAttribute('data-style-using'); 704 | } 705 | 706 | // loop through each provided style guide 707 | for (var i = 0, styleGuide; styleGuide = styleGuideSheet[i]; i++) { 708 | link = document.querySelector('link[href*="' + styleGuide + '"]'); 709 | 710 | if (!link) { 711 | continue; 712 | } 713 | 714 | sheet = link.sheet; 715 | 716 | getStyleSheetRules(sheet, function(rules, href) { 717 | 718 | forEachRule(rules, function(rule) { 719 | 720 | // deal with each selector individually since each selector can have it's own 721 | // level of specificity 722 | selectors = rule.selectorText.split(','); 723 | 724 | for (var y = 0; selector = selectors[y]; y++) { 725 | specificity = SPECIFICITY.calculate(selector)[0].specificity.split(',').map(Number); 726 | 727 | try { 728 | elms = document.body.querySelectorAll(selector); 729 | } 730 | catch(e) { 731 | return; 732 | } 733 | 734 | // loop through each element 735 | for (var j = 0, elmsLength = elms.length; j < elmsLength; j++) { 736 | el = elms[j]; 737 | 738 | // change the border of the element to show that it is using the style guide 739 | // only apply this to non-element only selectors so we don't have a page full 740 | // of borders 741 | if (compareSpecificity(specificity, [0,0,0,999999]) === specificity) { 742 | el.setAttribute('data-style-using', 'true'); 743 | } 744 | 745 | // loop through each rule property and check that the style guide styles 746 | // are being applied 747 | for (var k = 0, styleLength = rule.style.length; k < styleLength; k++) { 748 | property = rule.style[k]; 749 | value = getStyleValue(rule.style, property); 750 | elStyle = el.computedStyles[property]; 751 | 752 | if (elStyle[0].styleSheet !== href) { 753 | // make sure the styleSheet isn't in the ignore list 754 | var ignored = false; 755 | for (x = 0; ignore = ignoreSheet[x]; x++) { 756 | if (elStyle[0].styleSheet && elStyle[0].styleSheet.indexOf(ignore) !== -1) { 757 | ignored = true; 758 | break; 759 | } 760 | } 761 | 762 | if (!ignored) { 763 | el.problems = el.problems || []; 764 | 765 | var originalValue; 766 | var overrideValue; 767 | // convert rgb values to hex, but ignore any rgba values 768 | if (value.indexOf('rgb(') !== -1) { 769 | originalValue = rgbToHex.apply(this, value.match(rgbValues).map(Number)); 770 | } 771 | else { 772 | originalValue = value; 773 | } 774 | 775 | if (elStyle[0].value.indexOf('rgb(') !== -1) { 776 | overrideValue = rgbToHex.apply(this, elStyle[0].value.match(rgbValues).map(Number)); 777 | } 778 | else { 779 | overrideValue = elStyle[0].value; 780 | } 781 | 782 | el.problems.push({ 783 | type: 'property-override', 784 | selector: elStyle[0].selector, 785 | description: '' + property + ': ' + originalValue + ' overridden by ' + overrideValue + ' in the selector ' + elStyle[0].selector + ' from styleSheet ' + elStyle[0].styleSheet + '.', 786 | }); 787 | 788 | if (audit.elms.indexOf(el) === -1) { 789 | audit.elms.push(el); 790 | } 791 | } 792 | } 793 | } 794 | } 795 | } 796 | }); 797 | 798 | // change the background color of all elements 799 | for (var z = 0, elm; elm = audit.elms[z]; z++) { 800 | elm.setAttribute('data-style-audit', 'property-override'); 801 | } 802 | 803 | }); 804 | } 805 | 806 | // create the custom rule report 807 | for (i = 0; i < customRules.length; i++) { 808 | elms = document.body.querySelectorAll(customRules[i].selector); 809 | 810 | for (var j = 0; j < elms.length; j++) { 811 | elms[j].problems = elms[j].problems || []; 812 | elms[j].problems.push({ 813 | type: customRules[i].type, 814 | selector: customRules[i].selector, 815 | description: customRules[i].description 816 | }); 817 | elms[j].setAttribute('data-style-audit', customRules[i].type); 818 | } 819 | } 820 | 821 | // remove any styles from audit results 822 | elms = document.body.querySelectorAll('.audit-results *'); 823 | for (x = 0; elm = elms[x]; x++) { 824 | elm.removeAttribute('data-style-using'); 825 | elm.removeAttribute('data-style-audit'); 826 | } 827 | } 828 | 829 | window.auditStyleGuide = auditStyleGuide; 830 | /*jshint -W084 */ 831 | /* global container, auditTool, code, Prism */ 832 | 833 | /** 834 | * Escape <, >, and "" for output. 835 | * @param {string} str - String of HTML to escape. 836 | * @returns {string} 837 | * @see http://stackoverflow.com/questions/5406373/how-can-i-display-html-tags-inside-and-html-document 838 | */ 839 | function escapeHTML(str) { 840 | return str.replace(/>/g,'>').replace(/' + 871 | // TODO: add title "Don't show me again" when I have a way to ignore results 872 | '' + 873 | '' + 874 | '
' + 875 | // TODO: add title "Don't show me again" when I have a way to ignore results 876 | /*''*/ + 877 | '
'; 878 | frag.appendChild(li); 879 | } 880 | ul.appendChild(frag); 881 | container.appendChild(ul); 882 | } 883 | 884 | // setup a click handler on all audit elements to bring up a nice tray to display 885 | // the audit results 886 | document.body.addEventListener('click', function(e) { 887 | var el = e.target; 888 | 889 | if (!el) { 890 | return; 891 | } 892 | 893 | // walk the DOM tree looking for an element with the data-style-audit attribute 894 | do { 895 | if (el.getAttribute('data-style-audit') !== null) { 896 | e.preventDefault(); 897 | openAuditTool(el); 898 | return; 899 | } 900 | // if we clicked inside the audit-tool, don't close 901 | else if (el.classList.contains('audit-results')) { 902 | return; 903 | } 904 | } while (el = el.parentElement); 905 | 906 | // if no DOM found, close the tray 907 | try { 908 | document.body.classList.remove('open-audit'); 909 | } catch (error) {} 910 | }, true); 911 | /*jshint -W083 */ 912 | /*jshint -W084 */ 913 | /*jshint unused:false */ 914 | /* global getStyleSheetRules, forEachRule, SPECIFICITY, push, getStyleValue */ 915 | 916 | /** 917 | * Sort a computedStyle by specificity order 918 | * @param {object} a 919 | * @param {object} b 920 | * @returns {number} 921 | */ 922 | function specificitySort(a, b) { 923 | return b.specificity[0] - a.specificity[0] || 924 | b.specificity[1] - a.specificity[1] || 925 | b.specificity[2] - a.specificity[2] || 926 | b.specificity[3] - a.specificity[3] || 927 | b.index - a.index; 928 | } 929 | 930 | /** 931 | * Return the highest selector specificity. 932 | * @param {number[]} a 933 | * @param {number[]} b 934 | * @returns {number[]} 935 | */ 936 | function compareSpecificity(a, b) { 937 | for (var i = 0; i < 4; i++) { 938 | if (a[i] > b[i]) { return a; } 939 | if (b[i] > a[i]) { return b; } 940 | } 941 | 942 | // when both specificities tie, it doesn't matter which one is returned 943 | return a; 944 | } 945 | 946 | /** 947 | * Parse all the styleSheets on the page and determine which rules apply to which elements. 948 | */ 949 | function parseStyleSheets() { 950 | push.setAttribute('data-loading', 'true'); 951 | 952 | // allow the loading screen to show 953 | setTimeout(function() { 954 | // clear all previous parsing 955 | var all = document.body.querySelectorAll('[data-style-computed]'); 956 | var i, allLength, sheetLength; 957 | for (i = 0, allLength = all.length; i < allLength; i++) { 958 | all[i].computedStyles = {}; 959 | } 960 | 961 | var sheets = document.styleSheets; 962 | var count = 0; 963 | var sheet, selectors, selector, specificity, elms, el, property, value, elStyle; 964 | 965 | // loop through each styleSheet 966 | for (i = 0, sheetLength = sheets.length; i < sheetLength; i++) { 967 | sheet = sheets[i]; 968 | 969 | // create a closure for the styleSheet order so that we can resolve specificity ties 970 | // by the order in which the styleSheets are loaded on the page 971 | (function(index) { 972 | getStyleSheetRules(sheet, function(rules, href) { 973 | 974 | forEachRule(rules, function(rule) { 975 | // deal with each selector individually since each selector can have it's own 976 | // level of specificity 977 | selectors = rule.selectorText.split(','); 978 | 979 | for (var j = 0; selector = selectors[j]; j++) { 980 | specificity = SPECIFICITY.calculate(selector)[0].specificity.split(',').map(Number); 981 | 982 | try { 983 | elms = document.body.querySelectorAll(selector); 984 | } 985 | catch(e) { 986 | continue; 987 | } 988 | 989 | // loop through each element and set their computedStyles property 990 | for (var k = 0, elmsLength = elms.length; k < elmsLength; k++) { 991 | el = elms[k]; 992 | el.computedStyles = el.computedStyles || {}; 993 | 994 | // loop through each rule property and set the value in computedStyles 995 | for (var x = 0, styleLength = rule.style.length; x < styleLength; x++) { 996 | property = rule.style[x]; 997 | value = getStyleValue(rule.style, property); 998 | 999 | el.computedStyles[property] = el.computedStyles[property] || []; 1000 | elStyle = el.computedStyles[property]; 1001 | 1002 | // check that this selector isn't already being applied to this element 1003 | var ruleApplied = false; 1004 | for (var y = 0, elLength = elStyle.length; y < elLength; y++) { 1005 | if (elStyle[y].selector === rule.selectorText && 1006 | elStyle[y].styleSheet === href) { 1007 | 1008 | elStyle[y].specificity = compareSpecificity(elStyle[y].specificity, specificity); 1009 | ruleApplied = true; 1010 | break; 1011 | } 1012 | } 1013 | 1014 | if (!ruleApplied) { 1015 | elStyle.push({ 1016 | value: value, 1017 | styleSheet: href, 1018 | specificity: specificity, 1019 | selector: rule.selectorText, // we want the entire selector 1020 | index: index // order of the styleSheet for resolving specificity ties 1021 | }); 1022 | el.setAttribute('data-style-computed', 'true'); 1023 | } 1024 | 1025 | // sort property styles by specificity (i.e. how the browser would 1026 | // apply the style) 1027 | elStyle.sort(specificitySort); 1028 | } 1029 | } 1030 | } 1031 | }); 1032 | 1033 | // fire an event once all styleSheets have been parsed. 1034 | // this allows the auditResults() function to be called on the event 1035 | if (++count === sheetLength) { 1036 | push.removeAttribute('data-loading'); 1037 | var event = new CustomEvent('styleSheetsParsed', {count: count}); 1038 | document.dispatchEvent(event); 1039 | } 1040 | }); 1041 | })(i); 1042 | } 1043 | }, 250); 1044 | } 1045 | 1046 | window.parseStyleSheets = parseStyleSheets; 1047 | 1048 | })(window, document); -------------------------------------------------------------------------------- /index.min.js: -------------------------------------------------------------------------------- 1 | (function(e,t){"use strict";function r(e,r){var a,i=XMLHttpRequest,o=!1;try{o=XMLHttpRequest&&"withCredentials"in(a=new i)}catch(s){}return o?(a.open("GET",e),a.onload=function(){if(a.onload=a.onerror=null,a.status<200||a.status>=300)console.error("style failed to load: "+e);else{var i=t.createElement("style");i.appendChild(t.createTextNode(a.responseText)),i.setAttribute("data-url",e),t.head.appendChild(i),r(i),i.remove()}},a.onerror=function(){a.onload=a.onerror=null,console.error("XHR CORS CSS fail:"+e)},void a.send()):void console.error("CORS not supported")}function a(e){try{return e.cssRules||e.rules}catch(t){return}}function i(e,t){if(e.ownerNode&&e.ownerNode.hasAttribute("data-style-skip"))return void t([],e.href);var i=a(e);!i&&L[e.href]?(i=L[e.href].rules,t(i,e.href)):i?t(i,e.href):function(e){r(e.href,function(r){L[e.href]={},L[e.href].styleSheet=r.sheet,L[e.href].rules=a(r.sheet),t(L[e.href].rules,e.href)})}(e)}function o(e,t,r){for(var a,i=0,o=e.length;iMath.abs(t.deltaY)||!r)&&(r=Math.abs(t.deltaY)),t.stopPropagation?t.stopPropagation():t.cancelBubble=!0,t.deltaY<0&&0===e.scrollTop||t.deltaY>0&&e.scrollHeight===e.scrollTop+e.clientHeight?t.preventDefault?t.preventDefault():t.returnValue=!1:(o||(i=a.scrollTop),o++,setTimeout(function(){o--,o||a.scrollTop===i||(a.scrollTop=i)},0)))},n=function(e){e.deltaY=-e.wheelDelta,s(e)};e.removeWheelListener||e.addEventListener&&(e.addEventListener("wheel",s,!1),e.addEventListener("mousewheel",n,!1),e.removeWheelListener=function(){e.removeEventListener("wheel",s,!1),e.removeEventListener("mousewheel",n,!1),e.removeWheelListener=void 0})}function n(e,t,r){return"#"+((1<<24)+(e<<16)+(t<<8)+r).toString(16).slice(1)}function l(e){return e=e.replace(/-([a-z])/g,function(e){return e[1].toUpperCase()}),0===e.indexOf("Webkit")&&(e=e[0].toLowerCase()+e.slice(1)),e}function d(e,t){var r=e[t]||e[l(t)]||e[T[t]]||"";return r}function u(e,r,a){var s,l,u,c,p,f,g,h,y,b,v,x;Array.isArray(e)||(e=[e]),Array.isArray(r)||(r=[r]),a=a||[];for(var k=0;u=R.elms[k];k++)u.problems=[];for(R={elms:[]},c=t.body.querySelectorAll("[data-style-audit]"),k=0;u=c[k];k++)u.removeAttribute("data-style-audit");for(c=t.body.querySelectorAll("[data-style-using]"),k=0;u=c[k];k++)u.removeAttribute("data-style-using");for(var A,w=0;A=e[w];w++)s=t.querySelector('link[href*="'+A+'"]'),s&&(l=s.sheet,i(l,function(e,a){o(e,function(e){f=e.selectorText.split(",");for(var i=0;g=f[i];i++){h=_.calculate(g)[0].specificity.split(",").map(Number);try{c=t.body.querySelectorAll(g)}catch(o){return}for(var s=0,l=c.length;s"+y+": "+S+"
overridden by "+C+" in the selector "+v[0].selector+" from styleSheet "+v[0].styleSheet+"."}),R.elms.indexOf(p)===-1&&R.elms.push(p)}}}}});for(var i,s=0;i=R.elms[s];s++)i.setAttribute("data-style-audit","property-override")}));for(w=0;w/g,">").replace(/
'+i.description+"
",o.appendChild(a);s.appendChild(o),A.appendChild(s)}function f(e,t){return t.specificity[0]-e.specificity[0]||t.specificity[1]-e.specificity[1]||t.specificity[2]-e.specificity[2]||t.specificity[3]-e.specificity[3]||t.index-e.index}function m(e,t){for(var r=0;r<4;r++){if(e[r]>t[r])return e;if(t[r]>e[r])return t}return e}function g(){y.setAttribute("data-loading","true"),setTimeout(function(){var e,r,a,s=t.body.querySelectorAll("[data-style-computed]");for(e=0,r=s.length;e",t.body.appendChild(y);var b=t.createElement("div");b.setAttribute("class","audit-results"),t.body.appendChild(b),s(b);var v=t.createElement("code");v.setAttribute("class","language-markup");var x=t.createElement("pre");x.appendChild(v);var k=t.createElement("div");k.setAttribute("class","audit-results__title"),k.appendChild(x),b.appendChild(k);var A=t.createElement("div");A.setAttribute("class","audit-results__body"),b.appendChild(A);var w=t.createElement("style");w.setAttribute("data-style-skip","true");var S=".audit-results {position: fixed;bottom: -"+h+"px;left: 0;right: 0;height: "+h+"px;background: white;border-top: 0 solid black;transition: bottom 300ms, border 300ms;overflow-y: auto;z-index: 1000000}body.open-audit .audit-results {bottom: 0;border-top-width: 1px;}.audit-push-results {height: 0;transition: height 300ms;}.audit-push-results[data-loading] {position: fixed;left: 0;right: 0;top: 0;bottom: 0;background: rgba(166,166,166,.6);height: auto;z-index: 10000000;height: auto !important}.audit-push-results[data-loading] div {background-color: #fff;border-radius: 100%;margin: 2px;-webkit-animation-fill-mode: both;animation-fill-mode: both;border: 3px solid #fff;border-bottom-color: transparent;height: 100px;width: 100px;background: transparent !important;-webkit-animation: styleRotate 0.75s 0s linear infinite;animation: styleRotate 0.75s 0s linear infinite;position: absolute;top: 50%;left: 50%;margin-left: -50px;margin-top: -50px;}body.open-audit .audit-push-results {height: "+h+'px;}.audit-results__body {padding: 1em;}.audit-results__body li {margin-bottom: 10px;}.audit-results__body div:first-child {display: none;}.audit-results__body input[type="checkbox"] {float: none;margin: 0;padding: 0;}.audit-results__body label {font-size: 16px;padding-left: 0;}.audit-results__body code {margin-bottom: 4px;display: inline-block;}.audit-results pre[class*=language-] {border-radius: 0;margin: 0;}.audit-results pre[class*=language-]>code[data-language]::before {display: none;}[data-style-audit] {background: salmon !important;cursor: pointer !important;}[data-style-using] {outline: 1px dashed midnightblue !important}@keyframes styleRotate {0%, {-webkit-transform: rotate(0deg);transform: rotate(0deg);}50% {-webkit-transform: rotate(180deg);transform: rotate(180deg);}100% {-webkit-transform: rotate(360deg);transform: rotate(360deg);}}@-moz-keyframes styleRotate {0% {-webkit-transform: rotate(0deg);transform: rotate(0deg);}50% {-webkit-transform: rotate(180deg);transform: rotate(180deg);}100% {-webkit-transform: rotate(360deg);transform: rotate(360deg);}}@-webkit-keyframes styleRotate {0% {-webkit-transform: rotate(0deg);transform: rotate(0deg);}50% {-webkit-transform: rotate(180deg);transform: rotate(180deg);}100% {-webkit-transform: rotate(360deg);transform: rotate(360deg);}}@-o-keyframes styleRotate {0% {-webkit-transform: rotate(0deg);transform: rotate(0deg);}50% {-webkit-transform: rotate(180deg);transform: rotate(180deg);}100% {-webkit-transform: rotate(360deg);transform: rotate(360deg);}}@-ms-keyframes styleRotate {0% {-webkit-transform: rotate(0deg);transform: rotate(0deg);}50% {-webkit-transform: rotate(180deg);transform: rotate(180deg);}100% {-webkit-transform: rotate(360deg);transform: rotate(360deg);}}';if(w.appendChild(t.createTextNode(S)),t.head.appendChild(w),!e.Prism){var C=t.createElement("script");C.setAttribute("async",!0),C.src="https://cdnjs.cloudflare.com/ajax/libs/prism/0.0.1/prism.js",t.body.appendChild(C);var E=t.createElement("link");E.setAttribute("rel","stylesheet"),E.href="https://cdnjs.cloudflare.com/ajax/libs/prism/0.0.1/prism.min.css",E.setAttribute("data-style-skip","true"),t.head.appendChild(E)}var L={},T={"float":"cssFloat","margin-left-value":"marginLeft","margin-left-ltr-source":"","margin-left-rtl-source":"","margin-right-value":"marginRight","margin-right-ltr-source":"","margin-right-rtl-source":"","padding-right-value":"paddingRight","padding-right-ltr-source":"","padding-right-rtl-source":"","padding-left-value":"paddingLeft","padding-left-ltr-source":"","padding-left-rtl-source":""},_=function(){var e,t;return e=function(e){var r,a,i,o,s=[];for(r=e.split(","),i=0,o=r.length;i0&&s.push(t(a));return s},t=function(e){var t,r=e,a={a:0,b:0,c:0},i=[],o=/(\[[^\]]+\])/g,s=/(#[^\s\+>~\.\[:]+)/g,n=/(\.[^\s\+>~\.\[:]+)/g,l=/(::[^\s\+>~\.\[:]+|:first-line|:first-letter|:before|:after)/gi,d=/(:[\w-]+\([^\)]*\))/gi,u=/(:[^\s\+>~\.\[:]+)/g,c=/([^\s\+>~\.\[:]+)/g;return t=function(t,o){var s,n,l,d,u,c;if(t.test(r))for(s=r.match(t),n=0,l=s.length;n~]/g," "),r=r.replace(/[#\.]/g," "),t(c,"c"),i.sort(function(e,t){return e.index-t.index}),{selector:e,specificity:"0,"+a.a.toString()+","+a.b.toString()+","+a.c.toString(),parts:i}},{calculate:e}}();"undefined"!=typeof exports&&(exports.calculate=_.calculate);var R={elms:[]},H=/([0-9]){1,3}/g;e.auditStyleGuide=u,t.body.addEventListener("click",function(e){var r=e.target;if(r){do{if(null!==r.getAttribute("data-style-audit"))return e.preventDefault(),void p(r);if(r.classList.contains("audit-results"))return}while(r=r.parentElement);try{t.body.classList.remove("open-audit")}catch(a){}}},!0),e.parseStyleSheets=g})(window,document); -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | base: '', 4 | browsers: ['Chrome', 'Firefox', 'Safari', 'IE'], 5 | frameworks: ['jasmine', 'chai', 'sinon'], 6 | files: [ 7 | // dependencies 8 | {pattern: 'test/js/jquery-2.1.3.js', watched: false, served: true, included: true}, 9 | {pattern: 'node_modules/jasmine-jquery/lib/jasmine-jquery.js', watched: false, served: true, included: true}, 10 | 11 | // test html and css for fixtures 12 | {pattern: 'test/*.html', watched: true, served: true, included: false}, 13 | {pattern: 'test/css/*.css', watched: false, served: true, included: false}, 14 | 15 | // add css before index is run so it can be parsed and we can look at 16 | // the sheet property for the tests 17 | 'https://cdnjs.cloudflare.com/ajax/libs/pure/0.6.2/forms.css', 18 | 19 | 'index.test.js', 20 | 21 | // run in this order 22 | 'test/core.spec.js', 23 | 'test/parseStyleSheets.spec.js', 24 | 'test/auditStyleGuide.spec.js' 25 | ] 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-style-guide-audit", 3 | "version": "1.0.2", 4 | "description": "Audit the CSS on a page to ensure that no other styles are overriding the pattern library's styles", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/straker/css-style-guide-audit.git" 12 | }, 13 | "author": "", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/straker/css-style-guide-audit/issues" 17 | }, 18 | "homepage": "https://github.com/straker/css-style-guide-audit", 19 | "devDependencies": { 20 | "gulp": "^3.8.11", 21 | "gulp-concat-util": "^0.5.2", 22 | "gulp-jshint": "^1.9.2", 23 | "gulp-rename": "^1.2.0", 24 | "gulp-size": "^1.2.1", 25 | "gulp-uglify": "^1.1.0", 26 | "jshint-stylish": "^1.0.1", 27 | "karma": "~0.12.31", 28 | "karma-ie-launcher": "~0.1.5", 29 | "karma-firefox-launcher": "~0.1.4", 30 | "karma-chrome-launcher": "~0.1.7", 31 | "karma-safari-launcher": "~0.1.1", 32 | "jasmine-core": "~2.2.0", 33 | "karma-jasmine": "~0.3.5", 34 | "chai": "~2.2.0", 35 | "jasmine-jquery": "~2.0.6", 36 | "karma-chai": "~0.1.0", 37 | "sinon": "~1.14.1", 38 | "karma-sinon": "~1.0.4", 39 | "gulp-autoprefixer": "~2.1.0" 40 | }, 41 | "dependencies": { 42 | "specificity": "^0.1.4" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/auditStyleGuide.js: -------------------------------------------------------------------------------- 1 | /*jshint -W083 */ 2 | /*jshint -W084 */ 3 | /*jshint unused:false */ 4 | /* global console, getStyleSheetRules, forEachRule, rgbToHex, push, getStyleValue, compareSpecificity, SPECIFICITY */ 5 | 6 | var audit = {elms: []}; 7 | var rgbValues = /([0-9]){1,3}/g; 8 | 9 | /** 10 | * Produce a JSON audit. 11 | * @param {string|string[]} styleGuideSheet - href substring that uniquely identifies the style guide styleSheet(s) 12 | * @param {string|string[]} ignoreSheet - href substring that uniquely identifies any styleSheets that should be ignored in the audit 13 | * @param {object[]} customRules - Custom rules to be audited. 14 | * @example 15 | * // references any styleSheet that contains the text 'pattern-lib' 16 | * // e.g. localhost/css/pattern-lib.css 17 | * // e.g. http://myDomain/styles/pattern-lib-17D8401NDL.css 18 | * auditStyleGuide('pattern-lib'); 19 | */ 20 | function auditStyleGuide(styleGuideSheet, ignoreSheet, customRules) { 21 | var link, sheet, elm, elms, el, selectors, selector, specificity, property, value, elStyle, ignore; 22 | 23 | if (!Array.isArray(styleGuideSheet)) { 24 | styleGuideSheet = [styleGuideSheet]; 25 | } 26 | 27 | if (!Array.isArray(ignoreSheet)) { 28 | ignoreSheet = [ignoreSheet]; 29 | } 30 | 31 | customRules = customRules || []; 32 | 33 | // reset previous audit 34 | for (var x = 0; elm = audit.elms[x]; x++) { 35 | elm.problems = []; 36 | } 37 | audit = {elms: []}; 38 | 39 | elms = document.body.querySelectorAll('[data-style-audit]'); 40 | for (x = 0; elm = elms[x]; x++) { 41 | elm.removeAttribute('data-style-audit'); 42 | } 43 | 44 | elms = document.body.querySelectorAll('[data-style-using]'); 45 | for (x = 0; elm = elms[x]; x++) { 46 | elm.removeAttribute('data-style-using'); 47 | } 48 | 49 | // loop through each provided style guide 50 | for (var i = 0, styleGuide; styleGuide = styleGuideSheet[i]; i++) { 51 | link = document.querySelector('link[href*="' + styleGuide + '"]'); 52 | 53 | if (!link) { 54 | continue; 55 | } 56 | 57 | sheet = link.sheet; 58 | 59 | getStyleSheetRules(sheet, function(rules, href) { 60 | 61 | forEachRule(rules, function(rule) { 62 | 63 | // deal with each selector individually since each selector can have it's own 64 | // level of specificity 65 | selectors = rule.selectorText.split(','); 66 | 67 | for (var y = 0; selector = selectors[y]; y++) { 68 | specificity = SPECIFICITY.calculate(selector)[0].specificity.split(',').map(Number); 69 | 70 | try { 71 | elms = document.body.querySelectorAll(selector); 72 | } 73 | catch(e) { 74 | return; 75 | } 76 | 77 | // loop through each element 78 | for (var j = 0, elmsLength = elms.length; j < elmsLength; j++) { 79 | el = elms[j]; 80 | 81 | // change the border of the element to show that it is using the style guide 82 | // only apply this to non-element only selectors so we don't have a page full 83 | // of borders 84 | if (compareSpecificity(specificity, [0,0,0,999999]) === specificity) { 85 | el.setAttribute('data-style-using', 'true'); 86 | } 87 | 88 | // loop through each rule property and check that the style guide styles 89 | // are being applied 90 | for (var k = 0, styleLength = rule.style.length; k < styleLength; k++) { 91 | property = rule.style[k]; 92 | value = getStyleValue(rule.style, property); 93 | elStyle = el.computedStyles[property]; 94 | 95 | if (elStyle[0].styleSheet !== href) { 96 | // make sure the styleSheet isn't in the ignore list 97 | var ignored = false; 98 | for (x = 0; ignore = ignoreSheet[x]; x++) { 99 | if (elStyle[0].styleSheet && elStyle[0].styleSheet.indexOf(ignore) !== -1) { 100 | ignored = true; 101 | break; 102 | } 103 | } 104 | 105 | if (!ignored) { 106 | el.problems = el.problems || []; 107 | 108 | var originalValue; 109 | var overrideValue; 110 | // convert rgb values to hex, but ignore any rgba values 111 | if (value.indexOf('rgb(') !== -1) { 112 | originalValue = rgbToHex.apply(this, value.match(rgbValues).map(Number)); 113 | } 114 | else { 115 | originalValue = value; 116 | } 117 | 118 | if (elStyle[0].value.indexOf('rgb(') !== -1) { 119 | overrideValue = rgbToHex.apply(this, elStyle[0].value.match(rgbValues).map(Number)); 120 | } 121 | else { 122 | overrideValue = elStyle[0].value; 123 | } 124 | 125 | el.problems.push({ 126 | type: 'property-override', 127 | selector: elStyle[0].selector, 128 | description: '' + property + ': ' + originalValue + ' overridden by ' + overrideValue + ' in the selector ' + elStyle[0].selector + ' from styleSheet ' + elStyle[0].styleSheet + '.', 129 | }); 130 | 131 | if (audit.elms.indexOf(el) === -1) { 132 | audit.elms.push(el); 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | }); 140 | 141 | // change the background color of all elements 142 | for (var z = 0, elm; elm = audit.elms[z]; z++) { 143 | elm.setAttribute('data-style-audit', 'property-override'); 144 | } 145 | 146 | }); 147 | } 148 | 149 | // create the custom rule report 150 | for (i = 0; i < customRules.length; i++) { 151 | elms = document.body.querySelectorAll(customRules[i].selector); 152 | 153 | for (var j = 0; j < elms.length; j++) { 154 | elms[j].problems = elms[j].problems || []; 155 | elms[j].problems.push({ 156 | type: customRules[i].type, 157 | selector: customRules[i].selector, 158 | description: customRules[i].description 159 | }); 160 | elms[j].setAttribute('data-style-audit', customRules[i].type); 161 | } 162 | } 163 | 164 | // remove any styles from audit results 165 | elms = document.body.querySelectorAll('.audit-results *'); 166 | for (x = 0; elm = elms[x]; x++) { 167 | elm.removeAttribute('data-style-using'); 168 | elm.removeAttribute('data-style-audit'); 169 | } 170 | } 171 | 172 | window.auditStyleGuide = auditStyleGuide; -------------------------------------------------------------------------------- /src/auditView.js: -------------------------------------------------------------------------------- 1 | /*jshint -W084 */ 2 | /* global container, auditTool, code, Prism */ 3 | 4 | /** 5 | * Escape <, >, and "" for output. 6 | * @param {string} str - String of HTML to escape. 7 | * @returns {string} 8 | * @see http://stackoverflow.com/questions/5406373/how-can-i-display-html-tags-inside-and-html-document 9 | */ 10 | function escapeHTML(str) { 11 | return str.replace(/>/g,'>').replace(/' + 42 | // TODO: add title "Don't show me again" when I have a way to ignore results 43 | '' + 44 | '' + 45 | '
' + 46 | // TODO: add title "Don't show me again" when I have a way to ignore results 47 | /*''*/ + 48 | '
'; 49 | frag.appendChild(li); 50 | } 51 | ul.appendChild(frag); 52 | container.appendChild(ul); 53 | } 54 | 55 | // setup a click handler on all audit elements to bring up a nice tray to display 56 | // the audit results 57 | document.body.addEventListener('click', function(e) { 58 | var el = e.target; 59 | 60 | if (!el) { 61 | return; 62 | } 63 | 64 | // walk the DOM tree looking for an element with the data-style-audit attribute 65 | do { 66 | if (el.getAttribute('data-style-audit') !== null) { 67 | e.preventDefault(); 68 | openAuditTool(el); 69 | return; 70 | } 71 | // if we clicked inside the audit-tool, don't close 72 | else if (el.classList.contains('audit-results')) { 73 | return; 74 | } 75 | } while (el = el.parentElement); 76 | 77 | // if no DOM found, close the tray 78 | try { 79 | document.body.classList.remove('open-audit'); 80 | } catch (error) {} 81 | }, true); -------------------------------------------------------------------------------- /src/bookmarkletCore.js: -------------------------------------------------------------------------------- 1 | /* global parseStyleSheets */ 2 | 3 | // by bringing in the audit code from Dropbox, we can change the code at any time 4 | // and not have to force everyone to update their bookmarklet 5 | var script = document.createElement('script'); 6 | 7 | script.onload = function() { 8 | var push = document.querySelector('.audit-push-results'); 9 | push.removeAttribute('style'); 10 | push.firstChild.removeAttribute('style'); 11 | parseStyleSheets(); 12 | }; 13 | 14 | script.src = 'https://www.dropbox.com/s/vs4ny7igudc23jb/index-1.0.1.min.js?dl=0&raw=1'; 15 | document.head.appendChild(script); -------------------------------------------------------------------------------- /src/core.js: -------------------------------------------------------------------------------- 1 | /*jshint unused:false */ 2 | /*jshint latedef: nofunc */ 3 | /* global console */ 4 | 5 | var trayHeight = 300; 6 | 7 | // create a div that will push the content out of the way of the results tray 8 | var push = document.createElement('div'); 9 | push.classList.add('audit-push-results'); 10 | push.innerHTML = '
'; 11 | document.body.appendChild(push); 12 | 13 | // append the tray to body 14 | var auditTool = document.createElement('div'); 15 | auditTool.setAttribute('class', 'audit-results'); 16 | document.body.appendChild(auditTool); 17 | preventParentScroll(auditTool); 18 | 19 | // create a title for the tray 20 | var code = document.createElement('code'); 21 | code.setAttribute('class', 'language-markup'); 22 | var pre = document.createElement('pre'); 23 | pre.appendChild(code); 24 | 25 | var title = document.createElement('div'); 26 | title.setAttribute('class', 'audit-results__title'); 27 | title.appendChild(pre); 28 | auditTool.appendChild(title); 29 | 30 | // create a container for the results 31 | var container = document.createElement('div'); 32 | container.setAttribute('class', 'audit-results__body'); 33 | auditTool.appendChild(container); 34 | 35 | // append a styles for the tray to body 36 | var trayStyle = document.createElement('style'); 37 | trayStyle.setAttribute('data-style-skip', 'true'); 38 | var trayCss = '' + 39 | '.audit-results {' + 40 | 'position: fixed;' + 41 | 'bottom: -' + trayHeight + 'px;' + 42 | 'left: 0;' + 43 | 'right: 0;' + 44 | 'height: ' + trayHeight + 'px;' + 45 | 'background: white;' + 46 | 'border-top: 0 solid black;' + 47 | 'transition: bottom 300ms, border 300ms;' + 48 | 'overflow-y: auto;' + 49 | 'z-index: 1000000' + 50 | '}' + 51 | 'body.open-audit .audit-results {' + 52 | 'bottom: 0;' + 53 | 'border-top-width: 1px;' + 54 | '}' + 55 | '.audit-push-results {' + 56 | 'height: 0;' + 57 | 'transition: height 300ms;' + 58 | '}' + 59 | '.audit-push-results[data-loading] {' + 60 | 'position: fixed;' + 61 | 'left: 0;' + 62 | 'right: 0;' + 63 | 'top: 0;' + 64 | 'bottom: 0;' + 65 | 'background: rgba(166,166,166,.6);' + 66 | 'height: auto;' + 67 | 'z-index: 10000000;' + 68 | 'height: auto !important' + 69 | '}' + 70 | '.audit-push-results[data-loading] div {' + 71 | 'background-color: #fff;' + 72 | 'border-radius: 100%;' + 73 | 'margin: 2px;' + 74 | '-webkit-animation-fill-mode: both;' + 75 | 'animation-fill-mode: both;' + 76 | 'border: 3px solid #fff;' + 77 | 'border-bottom-color: transparent;' + 78 | 'height: 100px;' + 79 | 'width: 100px;' + 80 | 'background: transparent !important;' + 81 | '-webkit-animation: styleRotate 0.75s 0s linear infinite;' + 82 | 'animation: styleRotate 0.75s 0s linear infinite;' + 83 | 'position: absolute;' + 84 | 'top: 50%;' + 85 | 'left: 50%;' + 86 | 'margin-left: -50px;' + 87 | 'margin-top: -50px;' + 88 | '}' + 89 | 'body.open-audit .audit-push-results {' + 90 | 'height: ' + trayHeight + 'px;' + 91 | '}' + 92 | '.audit-results__body {' + 93 | 'padding: 1em;' + 94 | '}' + 95 | // TODO: add back when I have a way to ignore results 96 | // '.audit-results__body ul {' + 97 | // 'margin: 0;' + 98 | // '}' + 99 | '.audit-results__body li {' + 100 | 'margin-bottom: 10px;' + 101 | // TODO: add back when I have a way to ignore results 102 | // 'display: table;' + 103 | '}' + 104 | // TODO: add back when I have a way to ignore results 105 | // '.audit-results__body div {' + 106 | // 'display: table-cell;' + 107 | // '}' + 108 | '.audit-results__body div:first-child {' + // TODO: remove when I have a way to ignore results 109 | 'display: none;' + 110 | '}' + 111 | '.audit-results__body input[type="checkbox"] {' + 112 | 'float: none;' + 113 | 'margin: 0;' + 114 | 'padding: 0;' + 115 | '}' + 116 | '.audit-results__body label {' + 117 | 'font-size: 16px;' + 118 | 'padding-left: 0;' + // TODO: change back to 10px when I have a way to ignore results 119 | '}' + 120 | '.audit-results__body code {' + 121 | 'margin-bottom: 4px;' + 122 | 'display: inline-block;' + 123 | '}' + 124 | // override bootstrap and prism styles 125 | '.audit-results pre[class*=language-] {' + 126 | 'border-radius: 0;' + 127 | 'margin: 0;' + 128 | '}' + 129 | '.audit-results pre[class*=language-]>code[data-language]::before {' + 130 | 'display: none;' + 131 | '}' + 132 | // make all audit elements a different color 133 | '[data-style-audit] {' + 134 | 'background: salmon !important;' + 135 | 'cursor: pointer !important;' + 136 | '}' + 137 | // make the border of all elements using the style a different color 138 | '[data-style-using] {' + 139 | 'outline: 1px dashed midnightblue !important' + 140 | '}' + 141 | // rotate animation 142 | '@keyframes styleRotate {' + 143 | '0%, {' + 144 | '-webkit-transform: rotate(0deg);' + 145 | 'transform: rotate(0deg);' + 146 | '}' + 147 | '50% {' + 148 | '-webkit-transform: rotate(180deg);' + 149 | 'transform: rotate(180deg);' + 150 | '}' + 151 | '100% {' + 152 | '-webkit-transform: rotate(360deg);' + 153 | 'transform: rotate(360deg);' + 154 | '}' + 155 | '}' + 156 | '@-moz-keyframes styleRotate {' + 157 | '0% {' + 158 | '-webkit-transform: rotate(0deg);' + 159 | 'transform: rotate(0deg);' + 160 | '}' + 161 | '50% {' + 162 | '-webkit-transform: rotate(180deg);' + 163 | 'transform: rotate(180deg);' + 164 | '}' + 165 | '100% {' + 166 | '-webkit-transform: rotate(360deg);' + 167 | 'transform: rotate(360deg);' + 168 | '}' + 169 | '}' + 170 | '@-webkit-keyframes styleRotate {' + 171 | '0% {' + 172 | '-webkit-transform: rotate(0deg);' + 173 | 'transform: rotate(0deg);' + 174 | '}' + 175 | '50% {' + 176 | '-webkit-transform: rotate(180deg);' + 177 | 'transform: rotate(180deg);' + 178 | '}' + 179 | '100% {' + 180 | '-webkit-transform: rotate(360deg);' + 181 | 'transform: rotate(360deg);' + 182 | '}' + 183 | '}' + 184 | '@-o-keyframes styleRotate {' + 185 | '0% {' + 186 | '-webkit-transform: rotate(0deg);' + 187 | 'transform: rotate(0deg);' + 188 | '}' + 189 | '50% {' + 190 | '-webkit-transform: rotate(180deg);' + 191 | 'transform: rotate(180deg);' + 192 | '}' + 193 | '100% {' + 194 | '-webkit-transform: rotate(360deg);' + 195 | 'transform: rotate(360deg);' + 196 | '}' + 197 | '}' + 198 | '@-ms-keyframes styleRotate {' + 199 | '0% {' + 200 | '-webkit-transform: rotate(0deg);' + 201 | 'transform: rotate(0deg);' + 202 | '}' + 203 | '50% {' + 204 | '-webkit-transform: rotate(180deg);' + 205 | 'transform: rotate(180deg);' + 206 | '}' + 207 | '100% {' + 208 | '-webkit-transform: rotate(360deg);' + 209 | 'transform: rotate(360deg);' + 210 | '}' + 211 | '}'; 212 | trayStyle.appendChild(document.createTextNode(trayCss)); 213 | document.head.appendChild(trayStyle); 214 | 215 | // load prism.js syntax highlighting 216 | if (!window.Prism) { 217 | var prismJS = document.createElement('script'); 218 | prismJS.setAttribute('async', true); 219 | prismJS.src = 'https://cdnjs.cloudflare.com/ajax/libs/prism/0.0.1/prism.js'; 220 | document.body.appendChild(prismJS); 221 | var prismCSS = document.createElement('link'); 222 | prismCSS.setAttribute('rel', 'stylesheet'); 223 | prismCSS.href = 'https://cdnjs.cloudflare.com/ajax/libs/prism/0.0.1/prism.min.css'; 224 | prismCSS.setAttribute('data-style-skip', 'true'); 225 | document.head.appendChild(prismCSS); 226 | } 227 | 228 | /** 229 | * Load a styleSheet from a cross domain URL. 230 | * @param {string} url - The URL of the styleSheet to load. 231 | * @see http://stackoverflow.com/questions/3211536/accessing-cross-domain-style-sheet-with-cssrules 232 | */ 233 | function loadCSSCors(url, callback) { 234 | var XHR = XMLHttpRequest; 235 | var xhr; 236 | var hasCred = false; 237 | try { 238 | hasCred = XMLHttpRequest && ('withCredentials' in (xhr = new XHR())); 239 | } catch(e) {} 240 | 241 | if (!hasCred) { 242 | console.error('CORS not supported'); 243 | return; 244 | } 245 | 246 | xhr.open('GET', url); 247 | xhr.onload = function() { 248 | xhr.onload = xhr.onerror = null; 249 | if (xhr.status < 200 || xhr.status >=300) { 250 | console.error('style failed to load: ' + url); 251 | } 252 | else { 253 | var styleTag = document.createElement('style'); 254 | styleTag.appendChild(document.createTextNode(xhr.responseText)); 255 | styleTag.setAttribute('data-url', url); // set url for testing 256 | document.head.appendChild(styleTag); 257 | callback(styleTag); 258 | 259 | // clean up style tag when callback is finished 260 | styleTag.remove(); 261 | } 262 | }; 263 | xhr.onerror = function() { 264 | xhr.onload = xhr.onerror = null; 265 | console.error('XHR CORS CSS fail:' + url); 266 | }; 267 | xhr.send(); 268 | } 269 | 270 | /** 271 | * Wrapper function for getting a styleSheets rules 272 | * @param {CSSStyleSheet} sheet - The styleSheet to get the rules from. 273 | * @return {CSSRuleList} 274 | */ 275 | function getRules(sheet) { 276 | try { 277 | return sheet.cssRules || sheet.rules; 278 | } 279 | catch (e) { 280 | // Firefox will throw an insecure error when trying to look at the rules of a 281 | // cross domain styleSheet. We'll just eat the error and continue as the 282 | // code will automatically request the styleSheet through CORS to be able 283 | // to read it 284 | return; 285 | } 286 | } 287 | 288 | /** 289 | * Get a styleSheets rules object, taking into account styleSheets that are hosted on 290 | * different domains. 291 | * @param {CSSStyleSheet} sheet - The styleSheet to get the rules from. 292 | * @param {function} callback - Callback function to be called (needed for xhr CORS request) 293 | */ 294 | var styleSheets = {}; // keep a list of already requested styleSheets so we don't have to request them again 295 | function getStyleSheetRules(sheet, callback) { 296 | // skip any styleSheets we don't want to parse (e.g. prism.css, audit styles) 297 | if (sheet.ownerNode && sheet.ownerNode.hasAttribute('data-style-skip')) { 298 | callback([], sheet.href); 299 | return; 300 | } 301 | 302 | var rules = getRules(sheet); 303 | 304 | // check to see if we've already loaded this styleSheet 305 | if (!rules && styleSheets[sheet.href]) { 306 | rules = styleSheets[sheet.href].rules; 307 | 308 | callback(rules, sheet.href); 309 | } 310 | // this is an external styleSheet so we need to request it through CORS 311 | else if (!rules) { 312 | (function (sheet) { 313 | loadCSSCors(sheet.href, function(corsSheet) { 314 | styleSheets[sheet.href] = {}; 315 | styleSheets[sheet.href].styleSheet = corsSheet.sheet; 316 | styleSheets[sheet.href].rules = getRules(corsSheet.sheet); 317 | 318 | callback(styleSheets[sheet.href].rules, sheet.href); 319 | }); 320 | })(sheet); 321 | } 322 | else { 323 | callback(rules, sheet.href); 324 | } 325 | } 326 | 327 | /** 328 | * Iterate over a list of CSS rules and return only valid rules (e.g. no keyframe or 329 | * font-family declarations). 330 | * @param {CSSRuleList} rules - CSS rules to parse. 331 | * @see http://toddmotto.com/ditch-the-array-foreach-call-nodelist-hack/ 332 | */ 333 | function forEachRule(rules, callback, scope) { 334 | var rule; 335 | 336 | for (var i = 0, len = rules.length; i < len; i++) { 337 | rule = rules[i]; 338 | 339 | // keyframe and font-family declarations do not have selectorText 340 | if (!rule.selectorText) { 341 | continue; 342 | } 343 | 344 | callback.call(scope, rule, i); 345 | } 346 | } 347 | 348 | /** 349 | * Prevents a child element from scrolling a parent element (aka document). 350 | * @param {Element} element - Scrolling element. 351 | * @see http://codepen.io/Merri/pen/nhijD/ 352 | */ 353 | function preventParentScroll(element) { 354 | var html = document.getElementsByTagName('html')[0], 355 | htmlTop = 0, 356 | htmlBlockScroll = 0, 357 | minDeltaY, 358 | // this is where you put all your logic 359 | wheelHandler = function (e) { 360 | // do not prevent scrolling if element can't scroll 361 | if (element.scrollHeight <= element.clientHeight) { 362 | return; 363 | } 364 | 365 | // normalize Y delta 366 | if (minDeltaY > Math.abs(e.deltaY) || !minDeltaY) { 367 | minDeltaY = Math.abs(e.deltaY); 368 | } 369 | 370 | // prevent other wheel events and bubbling in general 371 | if(e.stopPropagation) { 372 | e.stopPropagation(); 373 | } else { 374 | e.cancelBubble = true; 375 | } 376 | 377 | // most often you want to prevent default scrolling behavior (full page scroll!) 378 | if( (e.deltaY < 0 && element.scrollTop === 0) || (e.deltaY > 0 && element.scrollHeight === element.scrollTop + element.clientHeight) ) { 379 | if(e.preventDefault) { 380 | e.preventDefault(); 381 | } else { 382 | e.returnValue = false; 383 | } 384 | } else { 385 | // safeguard against fast scroll in IE and mac 386 | if(!htmlBlockScroll) { 387 | htmlTop = html.scrollTop; 388 | } 389 | htmlBlockScroll++; 390 | // even IE11 updates scrollTop after the wheel event :/ 391 | setTimeout(function() { 392 | htmlBlockScroll--; 393 | if(!htmlBlockScroll && html.scrollTop !== htmlTop) { 394 | html.scrollTop = htmlTop; 395 | } 396 | }, 0); 397 | } 398 | }, 399 | // here we do only compatibility stuff 400 | mousewheelCompatibility = function (e) { 401 | // no need to convert more than this, we normalize the value anyway 402 | e.deltaY = -e.wheelDelta; 403 | // and then call our main handler 404 | wheelHandler(e); 405 | }; 406 | 407 | // do not add twice! 408 | if(element.removeWheelListener) { 409 | return; 410 | } 411 | 412 | if (element.addEventListener) { 413 | element.addEventListener('wheel', wheelHandler, false); 414 | element.addEventListener('mousewheel', mousewheelCompatibility, false); 415 | // expose a remove method 416 | element.removeWheelListener = function() { 417 | element.removeEventListener('wheel', wheelHandler, false); 418 | element.removeEventListener('mousewheel', mousewheelCompatibility, false); 419 | element.removeWheelListener = undefined; 420 | }; 421 | } 422 | } 423 | 424 | /** 425 | * Convert rgb values from the stylesheet to hex. 426 | * @param {number} r - Red value. 427 | * @param {number} g - Green value. 428 | * @param {number} b - Blue value. 429 | * @returns {string} 430 | * @see http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb 431 | */ 432 | function rgbToHex(r, g, b) { 433 | return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); 434 | } 435 | 436 | /** 437 | * Convert a hyphen-separated string into a camel case string. 438 | * @param {string} str - Hyphen-separated string. 439 | * @returns {string} 440 | */ 441 | function camelCase(str) { 442 | str = str.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); }); 443 | 444 | // webkit is lowercase in Chrome 445 | if (str.indexOf('Webkit') === 0) { 446 | str = str[0].toLowerCase() + str.slice(1); 447 | } 448 | 449 | return str; 450 | } 451 | 452 | // translate browser specific styles to their actual style. 453 | // @see https://gist.github.com/dennisroethig/7078659 454 | var propertyMap = { 455 | 'float': 'cssFloat', 456 | 'margin-left-value': 'marginLeft', 457 | 'margin-left-ltr-source': '', 458 | 'margin-left-rtl-source': '', 459 | 'margin-right-value': 'marginRight', 460 | 'margin-right-ltr-source': '', 461 | 'margin-right-rtl-source': '', 462 | 'padding-right-value': 'paddingRight', 463 | 'padding-right-ltr-source': '', 464 | 'padding-right-rtl-source': '', 465 | 'padding-left-value': 'paddingLeft', 466 | 'padding-left-ltr-source': '', 467 | 'padding-left-rtl-source': '' 468 | }; 469 | 470 | /** 471 | * Get a value from a style rule. 472 | * @param {CSS2Properties} style - CSS Style property object. 473 | * @param {string} property - CSS property name. 474 | */ 475 | function getStyleValue(style, property) { 476 | // Chrome maps hyphen-separated names to their camel case name 477 | // Firefox uses camel case names 478 | // if no value is found, default to "" so that .indexOf will still work 479 | var value = style[property] || style[ camelCase(property) ] || 480 | style[ propertyMap[property] ] || ''; 481 | 482 | return value; 483 | } -------------------------------------------------------------------------------- /src/parseStyleSheets.js: -------------------------------------------------------------------------------- 1 | /*jshint -W083 */ 2 | /*jshint -W084 */ 3 | /*jshint unused:false */ 4 | /* global getStyleSheetRules, forEachRule, SPECIFICITY, push, getStyleValue */ 5 | 6 | /** 7 | * Sort a computedStyle by specificity order 8 | * @param {object} a 9 | * @param {object} b 10 | * @returns {number} 11 | */ 12 | function specificitySort(a, b) { 13 | return b.specificity[0] - a.specificity[0] || 14 | b.specificity[1] - a.specificity[1] || 15 | b.specificity[2] - a.specificity[2] || 16 | b.specificity[3] - a.specificity[3] || 17 | b.index - a.index; 18 | } 19 | 20 | /** 21 | * Return the highest selector specificity. 22 | * @param {number[]} a 23 | * @param {number[]} b 24 | * @returns {number[]} 25 | */ 26 | function compareSpecificity(a, b) { 27 | for (var i = 0; i < 4; i++) { 28 | if (a[i] > b[i]) { return a; } 29 | if (b[i] > a[i]) { return b; } 30 | } 31 | 32 | // when both specificities tie, it doesn't matter which one is returned 33 | return a; 34 | } 35 | 36 | /** 37 | * Parse all the styleSheets on the page and determine which rules apply to which elements. 38 | */ 39 | function parseStyleSheets() { 40 | push.setAttribute('data-loading', 'true'); 41 | 42 | // allow the loading screen to show 43 | setTimeout(function() { 44 | // clear all previous parsing 45 | var all = document.body.querySelectorAll('[data-style-computed]'); 46 | var i, allLength, sheetLength; 47 | for (i = 0, allLength = all.length; i < allLength; i++) { 48 | all[i].computedStyles = {}; 49 | } 50 | 51 | var sheets = document.styleSheets; 52 | var count = 0; 53 | var sheet, selectors, selector, specificity, elms, el, property, value, elStyle; 54 | 55 | // loop through each styleSheet 56 | for (i = 0, sheetLength = sheets.length; i < sheetLength; i++) { 57 | sheet = sheets[i]; 58 | 59 | // create a closure for the styleSheet order so that we can resolve specificity ties 60 | // by the order in which the styleSheets are loaded on the page 61 | (function(index) { 62 | getStyleSheetRules(sheet, function(rules, href) { 63 | 64 | forEachRule(rules, function(rule) { 65 | // deal with each selector individually since each selector can have it's own 66 | // level of specificity 67 | selectors = rule.selectorText.split(','); 68 | 69 | for (var j = 0; selector = selectors[j]; j++) { 70 | specificity = SPECIFICITY.calculate(selector)[0].specificity.split(',').map(Number); 71 | 72 | try { 73 | elms = document.body.querySelectorAll(selector); 74 | } 75 | catch(e) { 76 | continue; 77 | } 78 | 79 | // loop through each element and set their computedStyles property 80 | for (var k = 0, elmsLength = elms.length; k < elmsLength; k++) { 81 | el = elms[k]; 82 | el.computedStyles = el.computedStyles || {}; 83 | 84 | // loop through each rule property and set the value in computedStyles 85 | for (var x = 0, styleLength = rule.style.length; x < styleLength; x++) { 86 | property = rule.style[x]; 87 | value = getStyleValue(rule.style, property); 88 | 89 | el.computedStyles[property] = el.computedStyles[property] || []; 90 | elStyle = el.computedStyles[property]; 91 | 92 | // check that this selector isn't already being applied to this element 93 | var ruleApplied = false; 94 | for (var y = 0, elLength = elStyle.length; y < elLength; y++) { 95 | if (elStyle[y].selector === rule.selectorText && 96 | elStyle[y].styleSheet === href) { 97 | 98 | elStyle[y].specificity = compareSpecificity(elStyle[y].specificity, specificity); 99 | ruleApplied = true; 100 | break; 101 | } 102 | } 103 | 104 | if (!ruleApplied) { 105 | elStyle.push({ 106 | value: value, 107 | styleSheet: href, 108 | specificity: specificity, 109 | selector: rule.selectorText, // we want the entire selector 110 | index: index // order of the styleSheet for resolving specificity ties 111 | }); 112 | el.setAttribute('data-style-computed', 'true'); 113 | } 114 | 115 | // sort property styles by specificity (i.e. how the browser would 116 | // apply the style) 117 | elStyle.sort(specificitySort); 118 | } 119 | } 120 | } 121 | }); 122 | 123 | // fire an event once all styleSheets have been parsed. 124 | // this allows the auditResults() function to be called on the event 125 | if (++count === sheetLength) { 126 | push.removeAttribute('data-loading'); 127 | var event = new CustomEvent('styleSheetsParsed', {count: count}); 128 | document.dispatchEvent(event); 129 | } 130 | }); 131 | })(i); 132 | } 133 | }, 250); 134 | } 135 | 136 | window.parseStyleSheets = parseStyleSheets; -------------------------------------------------------------------------------- /src/run.js: -------------------------------------------------------------------------------- 1 | // Your code here 2 | document.addEventListener('styleSheetsParsed', function() { 3 | // allow custom rules to be audited 4 | var auditRules = []; 5 | 6 | // auditStyleGuide('styleGuideSheet', ['ignoreSheet'], auditRules); 7 | }); 8 | -------------------------------------------------------------------------------- /test/auditStyleGuide.spec.js: -------------------------------------------------------------------------------- 1 | describe('auditStyleGuide', function() { 2 | // ensure this code has been run first 3 | parseStyleSheets(); 4 | 5 | it('should add the "data-style-audit" attribute to any elements that have a rule that overrides a styleSheet rule', function() { 6 | auditStyleGuide('patternLib'); 7 | 8 | expect($('[data-style-audit]').length).to.be.above(0); 9 | }); 10 | 11 | it('should add a problems property to an element that has a rule that overrides a styleSheet rule', function() { 12 | var button = $('button[data-style-audit]')[0]; 13 | 14 | expect(button.problems).to.exist; 15 | }); 16 | 17 | it('should add a problem with the correct data', function() { 18 | var button = $('button[data-style-audit]')[0]; 19 | 20 | // font-size, padding left/right/top/bottom 21 | // Firefox will add padding-left/right-value, padding-left/right-ltr-source, making it 9 in total 22 | expect(button.problems.length).to.be.at.least(5); 23 | 24 | // we want to only test that the properties exist, not what they are so that 25 | // they can change at any point in the future without breaking the test 26 | expect(button.problems[0].type).to.exist; 27 | expect(button.problems[0].selector).to.exist; 28 | expect(button.problems[0].description).to.exist; 29 | }); 30 | 31 | }); -------------------------------------------------------------------------------- /test/core.spec.js: -------------------------------------------------------------------------------- 1 | var expect = chai.expect; 2 | 3 | // add needed elements to the DOM 4 | var div = document.createElement('div'); 5 | div.innerHTML = '' + 6 | '' + 11 | '' + 12 | '' + 13 | '' + 14 | '' + 15 | ''; 16 | document.body.insertBefore(div, document.body.firstChild); 17 | 18 | /** 19 | * loadCSSCors 20 | */ 21 | describe('loadCSSCors', function () { 22 | var server; 23 | 24 | beforeEach(function() { 25 | server = sinon.fakeServer.create(); 26 | }); 27 | 28 | afterEach(function() { 29 | server.restore(); 30 | }); 31 | 32 | it('should make a GET request for the styleSheet', function() { 33 | loadCSSCors('css/patternLib.css'); 34 | 35 | expect(server.requests.length).to.equal(1); 36 | expect(server.requests[0].url).to.equal('css/patternLib.css'); 37 | }); 38 | 39 | it('should call the callback when the styleSheet has loaded', function() { 40 | var callback = sinon.spy(); 41 | 42 | loadCSSCors('css/patternLib.css', callback); 43 | 44 | server.requests[0].respond( 45 | 200, 46 | {"Content-Type": "text/css"}, 47 | 'body { font-size: 16px; }' 48 | ); 49 | 50 | expect(callback.calledOnce).to.be.true; 51 | }); 52 | 53 | it('should append the styleSheet to the DOM when loaded', function() { 54 | loadCSSCors('css/patternLib.css', function() { 55 | expect($('style[data-url="css/patternLib.css"]').length).to.equal(1); 56 | }); 57 | 58 | server.requests[0].respond( 59 | 200, 60 | {"Content-Type": "text/css"}, 61 | 'body { font-size: 16px; }' 62 | ); 63 | }); 64 | 65 | it('should cleanup the styleSheet after the callback', function() { 66 | loadCSSCors('css/patternLib.css', function() { 67 | 68 | }); 69 | 70 | server.requests[0].respond( 71 | 200, 72 | {"Content-Type": "text/css"}, 73 | 'body { font-size: 16px; }' 74 | ); 75 | 76 | expect($('style[data-url="css/patternLib.css"]').length).to.equal(0); 77 | }); 78 | }); 79 | 80 | 81 | 82 | 83 | 84 | /** 85 | * getRules 86 | */ 87 | describe('getRules', function() { 88 | var styleTag; 89 | 90 | beforeEach(function() { 91 | styleTag = document.createElement('style'); 92 | styleTag.appendChild(document.createTextNode('body { font-size: 16px; }')); 93 | document.head.appendChild(styleTag); 94 | }); 95 | 96 | afterEach(function() { 97 | styleTag.remove(); 98 | }); 99 | 100 | it('should return a CSSRuleList', function() { 101 | var rules = getRules(styleTag.sheet); 102 | expect(typeof rules).to.equal('object'); 103 | expect(rules instanceof CSSRuleList).to.be.true; 104 | }); 105 | 106 | it('should silently fail when requesting a CORS styleSheet', function() { 107 | var pureSheet = $('link[href*="pure"]')[0].sheet; 108 | var fn = function() { 109 | getRules(pureSheet); 110 | } 111 | 112 | expect(fn).to.not.throw(Error); 113 | }); 114 | }); 115 | 116 | 117 | 118 | 119 | 120 | /** 121 | * getStyleSheetRules 122 | */ 123 | describe('getStyleSheetRules', function() { 124 | var oldGetRules; 125 | 126 | describe('should', function() { 127 | beforeEach(function() { 128 | // trying to stub or spy on a function on window in Safari results in two 129 | // different functions, one on window and the other as just the function (e.g. 130 | // window.getRules !== getRules when window.getRules is stubbed or spied). 131 | // we can get around this by reassigning getRules to the stubbed version 132 | // on window. 133 | // oldGetRules = getRules; 134 | sinon.stub(window, 'getRules').returns(true); 135 | getRules = window.getRules; 136 | }); 137 | 138 | afterEach(function() { 139 | getRules.restore(); 140 | // calling restore on the function doesn't actually unpack it in Safari, 141 | // so we can get around this by just reassigning getRules to the old function 142 | getRules = window.getRules; 143 | }); 144 | 145 | it('call getRules', function() { 146 | var callback = sinon.stub(); 147 | getStyleSheetRules({href: true}, callback); 148 | 149 | expect(getRules.calledOnce).to.be.true; 150 | }); 151 | 152 | it('call the callback', function() { 153 | var callback = sinon.stub(); 154 | getStyleSheetRules({href: true}, callback); 155 | 156 | expect(callback.calledOnce).to.be.true; 157 | }); 158 | }); 159 | 160 | describe('should', function() { 161 | var link = $('link[href*="pure"]')[0]; 162 | 163 | beforeEach(function() { 164 | // see comments above 165 | sinon.stub(window, 'loadCSSCors', function(url, callback) { 166 | callback({sheet: 'pure.css'}); 167 | }); 168 | loadCSSCors = window.loadCSSCors; 169 | 170 | // remove the skip attribute so we will parse the styleSheet 171 | link.removeAttribute('data-style-skip'); 172 | }); 173 | 174 | afterEach(function() { 175 | // see comments above 176 | loadCSSCors.restore(); 177 | loadCSSCors = window.loadCSSCors; 178 | 179 | // reset styleSheets dictionary 180 | styleSheets = {}; 181 | 182 | // add back the skip attribute 183 | link.setAttribute('data-style-skip', 'true'); 184 | }); 185 | 186 | it('call loadCSSCors when looking at a CORS styleSheet', function() { 187 | var pureSheet = $('link[href*="pure"]')[0].sheet; 188 | var callback = sinon.stub(); 189 | getStyleSheetRules(pureSheet, callback); 190 | 191 | expect(loadCSSCors.calledOnce).to.be.true; 192 | }); 193 | 194 | it('call the callback when looking at a CORS styleSheet', function() { 195 | var pureSheet = $('link[href*="pure"]')[0].sheet; 196 | var callback = sinon.stub(); 197 | getStyleSheetRules(pureSheet, callback); 198 | 199 | expect(callback.calledOnce).to.be.true; 200 | }); 201 | 202 | it('add the CORS styleSheet to a dictionary for faster lookup if requested again', function() { 203 | 204 | 205 | getStyleSheetRules(link.sheet, function(rules, href){ 206 | expect(typeof styleSheets[href]).to.equal('object'); 207 | expect(styleSheets[href].styleSheet).to.equal('pure.css'); 208 | }); 209 | }); 210 | 211 | it('look at the dictionary for previously loaded CORS styleSheets', function() { 212 | var pureSheet = $('link[href*="pure"]')[0].sheet; 213 | getStyleSheetRules(pureSheet, function() {}); 214 | 215 | // modify dictionary to test if the 2nd call really does get this styleSheet 216 | styleSheets[pureSheet.href].styleSheet = 'notpure.css'; 217 | 218 | getStyleSheetRules(pureSheet, function(rules, href) { 219 | expect(typeof styleSheets[href]).to.equal('object'); 220 | expect(styleSheets[href].styleSheet).to.equal('notpure.css'); 221 | }); 222 | 223 | expect(loadCSSCors.calledOnce).to.be.true; 224 | }); 225 | }); 226 | }); 227 | 228 | 229 | 230 | 231 | 232 | /** 233 | * rgbToHex 234 | */ 235 | describe('rgbToHex', function() { 236 | 237 | it('should produce correct results', function() { 238 | expect( rgbToHex(0,0,0) ).to.equal('#000000'); 239 | expect( rgbToHex(255,255,255) ).to.equal('#ffffff'); 240 | expect( rgbToHex(51,51,49) ).to.equal('#333331'); 241 | expect( rgbToHex(77,77,74) ).to.equal('#4d4d4a'); 242 | expect( rgbToHex(37,164,186) ).to.equal('#25a4ba'); 243 | expect( rgbToHex(0, 21, 172) ).to.equal('#0015ac'); 244 | }); 245 | }); 246 | 247 | 248 | 249 | 250 | 251 | /** 252 | * camelCase 253 | */ 254 | describe('camelCase', function() { 255 | 256 | it('should produce correct results', function() { 257 | expect( camelCase('margin-left') ).to.equal('marginLeft'); 258 | expect( camelCase('font-size') ).to.equal('fontSize'); 259 | expect( camelCase('-webkit-transform') ).to.equal('webkitTransform'); 260 | expect( camelCase('-moz-box-sizing') ).to.equal('MozBoxSizing'); 261 | }); 262 | }); 263 | 264 | 265 | 266 | 267 | 268 | /** 269 | * getStyleValue 270 | */ 271 | describe('getStyleValue', function() { 272 | 273 | it('should return the style value by property name', function() { 274 | expect( getStyleValue({'color': '1'}, 'color') ).to.equal('1'); 275 | 276 | // test that it fallsback to camel case names 277 | expect( getStyleValue({'lineHeight': '1'}, 'line-height') ).to.equal('1'); 278 | expect( getStyleValue({'webkitTransform': '1'}, '-webkit-transform') ).to.equal('1'); 279 | expect( getStyleValue({'MozBoxSizing': '1'}, '-moz-box-sizing') ).to.equal('1'); 280 | 281 | // test that it fallsback to the propertyMap 282 | expect( getStyleValue({'cssFloat': '1'}, 'float') ).to.equal('1'); 283 | expect( getStyleValue({'marginRight': '1'}, 'margin-right-value') ).to.equal('1'); 284 | expect( getStyleValue({'marginRight': '1'}, 'margin-right-ltr-source') ).to.equal(''); 285 | 286 | // test that the default is an empty string 287 | expect( getStyleValue({'float': '1'}, 'line-height') ).to.equal(''); 288 | 289 | }); 290 | }); -------------------------------------------------------------------------------- /test/css/overrideStyles.css: -------------------------------------------------------------------------------- 1 | /* ensure a star selector doesn't break things */ 2 | *:not(button):not(div) { 3 | box-sizing: border-box; 4 | } 5 | 6 | .btn-override { 7 | font-size: 18px; 8 | padding: 17px 20px; 9 | } 10 | 11 | /* test all selectors 12 | taken from http://www.w3schools.com/cssref/, just run this code on the site: 13 | 14 | css = []; 15 | elms = document.querySelectorAll('.reference tr td:first-child'); 16 | for (var i = 0; i < elms.length; i++) { 17 | css.push(elms[i].textContent + ': inherit'); 18 | } 19 | css.join(';\n'); 20 | */ 21 | body { 22 | color: inherit; 23 | opacity: inherit; 24 | background: inherit; 25 | background-attachment: inherit; 26 | background-color: inherit; 27 | background-image: inherit; 28 | background-position: inherit; 29 | background-repeat: inherit; 30 | background-clip: inherit; 31 | background-origin: inherit; 32 | background-size: inherit; 33 | border: inherit; 34 | border-bottom: inherit; 35 | border-bottom-color: inherit; 36 | border-bottom-left-radius: inherit; 37 | border-bottom-right-radius: inherit; 38 | border-bottom-style: inherit; 39 | border-bottom-width: inherit; 40 | border-color: inherit; 41 | -o-border-image: inherit; 42 | border-image: inherit; 43 | border-image-outset: inherit; 44 | border-image-repeat: inherit; 45 | border-image-slice: inherit; 46 | border-image-source: inherit; 47 | border-image-width: inherit; 48 | border-left: inherit; 49 | border-left-color: inherit; 50 | border-left-style: inherit; 51 | border-left-width: inherit; 52 | border-radius: inherit; 53 | border-right: inherit; 54 | border-right-color: inherit; 55 | border-right-style: inherit; 56 | border-right-width: inherit; 57 | border-style: inherit; 58 | border-top: inherit; 59 | border-top-color: inherit; 60 | border-top-left-radius: inherit; 61 | border-top-right-radius: inherit; 62 | border-top-style: inherit; 63 | border-top-width: inherit; 64 | border-width: inherit; 65 | -webkit-box-decoration-break: inherit; 66 | box-decoration-break: inherit; 67 | box-shadow: inherit; 68 | bottom: inherit; 69 | clear: inherit; 70 | clip: inherit; 71 | display: inherit; 72 | float: inherit; 73 | height: inherit; 74 | left: inherit; 75 | overflow: inherit; 76 | overflow-x: inherit; 77 | overflow-y: inherit; 78 | padding: inherit; 79 | padding-bottom: inherit; 80 | padding-left: inherit; 81 | padding-right: inherit; 82 | padding-top: inherit; 83 | position: inherit; 84 | right: inherit; 85 | top: inherit; 86 | visibility: inherit; 87 | width: inherit; 88 | vertical-align: inherit; 89 | z-index: inherit; 90 | -ms-flex-line-pack: inherit; 91 | align-content: inherit; 92 | -webkit-box-align: inherit; 93 | -ms-flex-align: inherit; 94 | align-items: inherit; 95 | -ms-flex-item-align: inherit; 96 | align-self: inherit; 97 | display: inherit; 98 | -webkit-box-flex: inherit; 99 | -ms-flex: inherit; 100 | flex: inherit; 101 | -ms-flex-preferred-size: inherit; 102 | flex-basis: inherit; 103 | -webkit-box-orient: vertical; 104 | -webkit-box-direction: normal; 105 | -ms-flex-direction: inherit; 106 | flex-direction: inherit; 107 | -ms-flex-flow: inherit; 108 | flex-flow: inherit; 109 | -webkit-box-flex: inherit; 110 | -webkit-flex-grow: inherit; 111 | -ms-flex-positive: inherit; 112 | flex-grow: inherit; 113 | -ms-flex-negative: inherit; 114 | flex-shrink: inherit; 115 | -ms-flex-wrap: inherit; 116 | flex-wrap: inherit; 117 | -webkit-box-pack: inherit; 118 | -ms-flex-pack: inherit; 119 | justify-content: inherit; 120 | margin: inherit; 121 | margin-bottom: inherit; 122 | margin-left: inherit; 123 | margin-right: inherit; 124 | margin-top: inherit; 125 | max-height: inherit; 126 | max-width: inherit; 127 | min-height: inherit; 128 | min-width: inherit; 129 | -webkit-box-ordinal-group: NaN; 130 | -ms-flex-order: inherit; 131 | order: inherit; 132 | hanging-punctuation: inherit; 133 | -webkit-hyphens: inherit; 134 | -moz-hyphens: inherit; 135 | -ms-hyphens: inherit; 136 | hyphens: inherit; 137 | letter-spacing: inherit; 138 | line-break: inherit; 139 | line-height: inherit; 140 | overflow-wrap: inherit; 141 | -moz-tab-size: inherit; 142 | -o-tab-size: inherit; 143 | tab-size: inherit; 144 | text-align: inherit; 145 | -moz-text-align-last: inherit; 146 | text-align-last: inherit; 147 | text-combine-upright: inherit; 148 | text-indent: inherit; 149 | text-justify: inherit; 150 | text-transform: inherit; 151 | white-space: inherit; 152 | word-break: inherit; 153 | word-spacing: inherit; 154 | word-wrap: inherit; 155 | text-decoration: inherit; 156 | -moz-text-decoration-color: inherit; 157 | -webkit-text-decoration-color: inherit; 158 | text-decoration-color: inherit; 159 | -moz-text-decoration-line: inherit; 160 | -webkit-text-decoration-line: inherit; 161 | text-decoration-line: inherit; 162 | -webkit-text-decoration-style: inherit; 163 | -moz-text-decoration-style: inherit; 164 | text-decoration-style: inherit; 165 | text-shadow: inherit; 166 | text-underline-position: inherit; 167 | font: inherit; 168 | font-family: inherit; 169 | -webkit-font-feature-settings: inherit; 170 | -moz-font-feature-settings: inherit; 171 | font-feature-settings: inherit; 172 | -webkit-font-kerning: inherit; 173 | -moz-font-kerning: inherit; 174 | font-kerning: inherit; 175 | -webkit-font-language-override: inherit; 176 | -moz-font-language-override: inherit; 177 | font-language-override: inherit; 178 | font-size: inherit; 179 | font-size-adjust: inherit; 180 | font-stretch: inherit; 181 | font-style: inherit; 182 | font-synthesis: inherit; 183 | font-variant: inherit; 184 | font-variant-alternates: inherit; 185 | font-variant-caps: inherit; 186 | font-variant-east-asian: inherit; 187 | -webkit-font-variant-ligatures: inherit; 188 | -moz-font-variant-ligatures: inherit; 189 | font-variant-ligatures: inherit; 190 | font-variant-numeric: inherit; 191 | font-variant-position: inherit; 192 | font-weight: inherit; 193 | direction: inherit; 194 | text-orientation: inherit; 195 | text-combine-upright: inherit; 196 | unicode-bidi: inherit; 197 | writing-mode: inherit; 198 | border-collapse: inherit; 199 | border-spacing: inherit; 200 | caption-side: inherit; 201 | empty-cells: inherit; 202 | table-layout: inherit; 203 | counter-increment: inherit; 204 | counter-reset: inherit; 205 | list-style: inherit; 206 | list-style-image: inherit; 207 | list-style-position: inherit; 208 | list-style-type: inherit; 209 | -webkit-animation: inherit; 210 | animation: inherit; 211 | -webkit-animation-delay: inherit; 212 | animation-delay: inherit; 213 | -webkit-animation-direction: inherit; 214 | animation-direction: inherit; 215 | -webkit-animation-duration: inherit; 216 | animation-duration: inherit; 217 | -webkit-animation-fill-mode: inherit; 218 | animation-fill-mode: inherit; 219 | -webkit-animation-iteration-count: inherit; 220 | animation-iteration-count: inherit; 221 | -webkit-animation-name: inherit; 222 | animation-name: inherit; 223 | -webkit-animation-play-state: inherit; 224 | animation-play-state: inherit; 225 | -webkit-animation-timing-function: inherit; 226 | animation-timing-function: inherit; 227 | -webkit-backface-visibility: inherit; 228 | backface-visibility: inherit; 229 | -webkit-perspective: inherit; 230 | perspective: inherit; 231 | -webkit-perspective-origin: inherit; 232 | perspective-origin: inherit; 233 | -webkit-transform: inherit; 234 | transform: inherit; 235 | -webkit-transform-origin: inherit; 236 | transform-origin: inherit; 237 | -webkit-transform-style: inherit; 238 | transform-style: inherit; 239 | -webkit-transition: inherit; 240 | transition: inherit; 241 | -webkit-transition-property: inherit; 242 | transition-property: inherit; 243 | -webkit-transition-duration: inherit; 244 | transition-duration: inherit; 245 | -webkit-transition-timing-function: inherit; 246 | transition-timing-function: inherit; 247 | -webkit-transition-delay: inherit; 248 | transition-delay: inherit; 249 | box-sizing: inherit; 250 | content: inherit; 251 | cursor: inherit; 252 | ime-mode: inherit; 253 | nav-down: inherit; 254 | nav-index: inherit; 255 | nav-left: inherit; 256 | nav-right: inherit; 257 | nav-up: inherit; 258 | outline: inherit; 259 | outline-color: inherit; 260 | outline-offset: inherit; 261 | outline-style: inherit; 262 | outline-width: inherit; 263 | resize: inherit; 264 | text-overflow: inherit; 265 | -webkit-break-after: inherit; 266 | -moz-break-after: inherit; 267 | break-after: inherit; 268 | -webkit-break-before: inherit; 269 | -moz-break-before: inherit; 270 | break-before: inherit; 271 | -webkit-column-break-inside: inherit; 272 | page-break-inside: inherit; 273 | break-inside: inherit; 274 | -webkit-column-count: inherit; 275 | -moz-column-count: inherit; 276 | column-count: inherit; 277 | -webkit-column-fill: inherit; 278 | -moz-column-fill: inherit; 279 | column-fill: inherit; 280 | -webkit-column-gap: inherit; 281 | -moz-column-gap: inherit; 282 | column-gap: inherit; 283 | -webkit-column-rule: inherit; 284 | -moz-column-rule: inherit; 285 | column-rule: inherit; 286 | -webkit-column-rule-color: inherit; 287 | -moz-column-rule-color: inherit; 288 | column-rule-color: inherit; 289 | -webkit-column-rule-style: inherit; 290 | -moz-column-rule-style: inherit; 291 | column-rule-style: inherit; 292 | -webkit-column-rule-width: inherit; 293 | -moz-column-rule-width: inherit; 294 | column-rule-width: inherit; 295 | -webkit-column-span: inherit; 296 | -moz-column-span: inherit; 297 | column-span: inherit; 298 | -webkit-column-width: inherit; 299 | -moz-column-width: inherit; 300 | column-width: inherit; 301 | -webkit-columns: inherit; 302 | -moz-columns: inherit; 303 | columns: inherit; 304 | widows: inherit; 305 | orphans: inherit; 306 | page-break-after: inherit; 307 | page-break-before: inherit; 308 | page-break-inside: inherit; 309 | marks: inherit; 310 | quotes: inherit; 311 | -webkit-filter: inherit; 312 | filter: inherit; 313 | image-orientation: inherit; 314 | image-rendering: inherit; 315 | image-resolution: inherit; 316 | -o-object-fit: inherit; 317 | object-fit: inherit; 318 | -o-object-position: inherit; 319 | object-position: inherit; 320 | -webkit-mask: inherit; 321 | mask: inherit; 322 | mask-type: inherit; 323 | mark: inherit; 324 | mark-after: inherit; 325 | mark-before: inherit; 326 | phonemes: inherit; 327 | rest: inherit; 328 | rest-after: inherit; 329 | rest-before: inherit; 330 | voice-balance: inherit; 331 | voice-duration: inherit; 332 | voice-pitch: inherit; 333 | voice-pitch-range: inherit; 334 | voice-rate: inherit; 335 | voice-stress: inherit; 336 | voice-volume: inherit; 337 | marquee-direction: inherit; 338 | marquee-play-count: inherit; 339 | marquee-speed: inherit; 340 | marquee-style: inherit 341 | } -------------------------------------------------------------------------------- /test/css/patternLib.css: -------------------------------------------------------------------------------- 1 | .pattern-lib-btn { 2 | font-size: 14px; 3 | line-height: 1; 4 | border-radius: 4px; 5 | padding: 9.5px 20px; 6 | position: relative; 7 | border: none; 8 | color: #fff; 9 | display: inline-block; 10 | white-space: nowrap; 11 | vertical-align: middle; 12 | } 13 | 14 | /* test all selectors 15 | taken from http://www.w3schools.com/cssref/, just run this code on the site: 16 | 17 | css = []; 18 | elms = document.querySelectorAll('.reference tr td:first-child'); 19 | for (var i = 0; i < elms.length; i++) { 20 | css.push(elms[i].textContent + ': inherit'); 21 | } 22 | css.join(';\n'); 23 | */ 24 | body { 25 | color: inherit; 26 | opacity: inherit; 27 | background: inherit; 28 | background-attachment: inherit; 29 | background-color: inherit; 30 | background-image: inherit; 31 | background-position: inherit; 32 | background-repeat: inherit; 33 | background-clip: inherit; 34 | background-origin: inherit; 35 | background-size: inherit; 36 | border: inherit; 37 | border-bottom: inherit; 38 | border-bottom-color: inherit; 39 | border-bottom-left-radius: inherit; 40 | border-bottom-right-radius: inherit; 41 | border-bottom-style: inherit; 42 | border-bottom-width: inherit; 43 | border-color: inherit; 44 | -o-border-image: inherit; 45 | border-image: inherit; 46 | border-image-outset: inherit; 47 | border-image-repeat: inherit; 48 | border-image-slice: inherit; 49 | border-image-source: inherit; 50 | border-image-width: inherit; 51 | border-left: inherit; 52 | border-left-color: inherit; 53 | border-left-style: inherit; 54 | border-left-width: inherit; 55 | border-radius: inherit; 56 | border-right: inherit; 57 | border-right-color: inherit; 58 | border-right-style: inherit; 59 | border-right-width: inherit; 60 | border-style: inherit; 61 | border-top: inherit; 62 | border-top-color: inherit; 63 | border-top-left-radius: inherit; 64 | border-top-right-radius: inherit; 65 | border-top-style: inherit; 66 | border-top-width: inherit; 67 | border-width: inherit; 68 | -webkit-box-decoration-break: inherit; 69 | box-decoration-break: inherit; 70 | box-shadow: inherit; 71 | bottom: inherit; 72 | clear: inherit; 73 | clip: inherit; 74 | display: inherit; 75 | float: inherit; 76 | height: inherit; 77 | left: inherit; 78 | overflow: inherit; 79 | overflow-x: inherit; 80 | overflow-y: inherit; 81 | padding: inherit; 82 | padding-bottom: inherit; 83 | padding-left: inherit; 84 | padding-right: inherit; 85 | padding-top: inherit; 86 | position: inherit; 87 | right: inherit; 88 | top: inherit; 89 | visibility: inherit; 90 | width: inherit; 91 | vertical-align: inherit; 92 | z-index: inherit; 93 | -ms-flex-line-pack: inherit; 94 | align-content: inherit; 95 | -webkit-box-align: inherit; 96 | -ms-flex-align: inherit; 97 | align-items: inherit; 98 | -ms-flex-item-align: inherit; 99 | align-self: inherit; 100 | display: inherit; 101 | -webkit-box-flex: inherit; 102 | -ms-flex: inherit; 103 | flex: inherit; 104 | -ms-flex-preferred-size: inherit; 105 | flex-basis: inherit; 106 | -webkit-box-orient: vertical; 107 | -webkit-box-direction: normal; 108 | -ms-flex-direction: inherit; 109 | flex-direction: inherit; 110 | -ms-flex-flow: inherit; 111 | flex-flow: inherit; 112 | -webkit-box-flex: inherit; 113 | -webkit-flex-grow: inherit; 114 | -ms-flex-positive: inherit; 115 | flex-grow: inherit; 116 | -ms-flex-negative: inherit; 117 | flex-shrink: inherit; 118 | -ms-flex-wrap: inherit; 119 | flex-wrap: inherit; 120 | -webkit-box-pack: inherit; 121 | -ms-flex-pack: inherit; 122 | justify-content: inherit; 123 | margin: inherit; 124 | margin-bottom: inherit; 125 | margin-left: inherit; 126 | margin-right: inherit; 127 | margin-top: inherit; 128 | max-height: inherit; 129 | max-width: inherit; 130 | min-height: inherit; 131 | min-width: inherit; 132 | -webkit-box-ordinal-group: NaN; 133 | -ms-flex-order: inherit; 134 | order: inherit; 135 | hanging-punctuation: inherit; 136 | -webkit-hyphens: inherit; 137 | -moz-hyphens: inherit; 138 | -ms-hyphens: inherit; 139 | hyphens: inherit; 140 | letter-spacing: inherit; 141 | line-break: inherit; 142 | line-height: inherit; 143 | overflow-wrap: inherit; 144 | -moz-tab-size: inherit; 145 | -o-tab-size: inherit; 146 | tab-size: inherit; 147 | text-align: inherit; 148 | -moz-text-align-last: inherit; 149 | text-align-last: inherit; 150 | text-combine-upright: inherit; 151 | text-indent: inherit; 152 | text-justify: inherit; 153 | text-transform: inherit; 154 | white-space: inherit; 155 | word-break: inherit; 156 | word-spacing: inherit; 157 | word-wrap: inherit; 158 | text-decoration: inherit; 159 | -moz-text-decoration-color: inherit; 160 | -webkit-text-decoration-color: inherit; 161 | text-decoration-color: inherit; 162 | -moz-text-decoration-line: inherit; 163 | -webkit-text-decoration-line: inherit; 164 | text-decoration-line: inherit; 165 | -webkit-text-decoration-style: inherit; 166 | -moz-text-decoration-style: inherit; 167 | text-decoration-style: inherit; 168 | text-shadow: inherit; 169 | text-underline-position: inherit; 170 | font: inherit; 171 | font-family: inherit; 172 | -webkit-font-feature-settings: inherit; 173 | -moz-font-feature-settings: inherit; 174 | font-feature-settings: inherit; 175 | -webkit-font-kerning: inherit; 176 | -moz-font-kerning: inherit; 177 | font-kerning: inherit; 178 | -webkit-font-language-override: inherit; 179 | -moz-font-language-override: inherit; 180 | font-language-override: inherit; 181 | font-size: inherit; 182 | font-size-adjust: inherit; 183 | font-stretch: inherit; 184 | font-style: inherit; 185 | font-synthesis: inherit; 186 | font-variant: inherit; 187 | font-variant-alternates: inherit; 188 | font-variant-caps: inherit; 189 | font-variant-east-asian: inherit; 190 | -webkit-font-variant-ligatures: inherit; 191 | -moz-font-variant-ligatures: inherit; 192 | font-variant-ligatures: inherit; 193 | font-variant-numeric: inherit; 194 | font-variant-position: inherit; 195 | font-weight: inherit; 196 | direction: inherit; 197 | text-orientation: inherit; 198 | text-combine-upright: inherit; 199 | unicode-bidi: inherit; 200 | writing-mode: inherit; 201 | border-collapse: inherit; 202 | border-spacing: inherit; 203 | caption-side: inherit; 204 | empty-cells: inherit; 205 | table-layout: inherit; 206 | counter-increment: inherit; 207 | counter-reset: inherit; 208 | list-style: inherit; 209 | list-style-image: inherit; 210 | list-style-position: inherit; 211 | list-style-type: inherit; 212 | -webkit-animation: inherit; 213 | animation: inherit; 214 | -webkit-animation-delay: inherit; 215 | animation-delay: inherit; 216 | -webkit-animation-direction: inherit; 217 | animation-direction: inherit; 218 | -webkit-animation-duration: inherit; 219 | animation-duration: inherit; 220 | -webkit-animation-fill-mode: inherit; 221 | animation-fill-mode: inherit; 222 | -webkit-animation-iteration-count: inherit; 223 | animation-iteration-count: inherit; 224 | -webkit-animation-name: inherit; 225 | animation-name: inherit; 226 | -webkit-animation-play-state: inherit; 227 | animation-play-state: inherit; 228 | -webkit-animation-timing-function: inherit; 229 | animation-timing-function: inherit; 230 | -webkit-backface-visibility: inherit; 231 | backface-visibility: inherit; 232 | -webkit-perspective: inherit; 233 | perspective: inherit; 234 | -webkit-perspective-origin: inherit; 235 | perspective-origin: inherit; 236 | -webkit-transform: inherit; 237 | transform: inherit; 238 | -webkit-transform-origin: inherit; 239 | transform-origin: inherit; 240 | -webkit-transform-style: inherit; 241 | transform-style: inherit; 242 | -webkit-transition: inherit; 243 | transition: inherit; 244 | -webkit-transition-property: inherit; 245 | transition-property: inherit; 246 | -webkit-transition-duration: inherit; 247 | transition-duration: inherit; 248 | -webkit-transition-timing-function: inherit; 249 | transition-timing-function: inherit; 250 | -webkit-transition-delay: inherit; 251 | transition-delay: inherit; 252 | box-sizing: inherit; 253 | content: inherit; 254 | cursor: inherit; 255 | ime-mode: inherit; 256 | nav-down: inherit; 257 | nav-index: inherit; 258 | nav-left: inherit; 259 | nav-right: inherit; 260 | nav-up: inherit; 261 | outline: inherit; 262 | outline-color: inherit; 263 | outline-offset: inherit; 264 | outline-style: inherit; 265 | outline-width: inherit; 266 | resize: inherit; 267 | text-overflow: inherit; 268 | -webkit-break-after: inherit; 269 | -moz-break-after: inherit; 270 | break-after: inherit; 271 | -webkit-break-before: inherit; 272 | -moz-break-before: inherit; 273 | break-before: inherit; 274 | -webkit-column-break-inside: inherit; 275 | page-break-inside: inherit; 276 | break-inside: inherit; 277 | -webkit-column-count: inherit; 278 | -moz-column-count: inherit; 279 | column-count: inherit; 280 | -webkit-column-fill: inherit; 281 | -moz-column-fill: inherit; 282 | column-fill: inherit; 283 | -webkit-column-gap: inherit; 284 | -moz-column-gap: inherit; 285 | column-gap: inherit; 286 | -webkit-column-rule: inherit; 287 | -moz-column-rule: inherit; 288 | column-rule: inherit; 289 | -webkit-column-rule-color: inherit; 290 | -moz-column-rule-color: inherit; 291 | column-rule-color: inherit; 292 | -webkit-column-rule-style: inherit; 293 | -moz-column-rule-style: inherit; 294 | column-rule-style: inherit; 295 | -webkit-column-rule-width: inherit; 296 | -moz-column-rule-width: inherit; 297 | column-rule-width: inherit; 298 | -webkit-column-span: inherit; 299 | -moz-column-span: inherit; 300 | column-span: inherit; 301 | -webkit-column-width: inherit; 302 | -moz-column-width: inherit; 303 | column-width: inherit; 304 | -webkit-columns: inherit; 305 | -moz-columns: inherit; 306 | columns: inherit; 307 | widows: inherit; 308 | orphans: inherit; 309 | page-break-after: inherit; 310 | page-break-before: inherit; 311 | page-break-inside: inherit; 312 | marks: inherit; 313 | quotes: inherit; 314 | -webkit-filter: inherit; 315 | filter: inherit; 316 | image-orientation: inherit; 317 | image-rendering: inherit; 318 | image-resolution: inherit; 319 | -o-object-fit: inherit; 320 | object-fit: inherit; 321 | -o-object-position: inherit; 322 | object-position: inherit; 323 | -webkit-mask: inherit; 324 | mask: inherit; 325 | mask-type: inherit; 326 | mark: inherit; 327 | mark-after: inherit; 328 | mark-before: inherit; 329 | phonemes: inherit; 330 | rest: inherit; 331 | rest-after: inherit; 332 | rest-before: inherit; 333 | voice-balance: inherit; 334 | voice-duration: inherit; 335 | voice-pitch: inherit; 336 | voice-pitch-range: inherit; 337 | voice-rate: inherit; 338 | voice-stress: inherit; 339 | voice-volume: inherit; 340 | marquee-direction: inherit; 341 | marquee-play-count: inherit; 342 | marquee-speed: inherit; 343 | marquee-style: inherit 344 | } -------------------------------------------------------------------------------- /test/parseStyleSheets.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * specificitySort 3 | */ 4 | var spec0 = {specificity: [0,0,0,0], index: 0}; 5 | var spec1 = {specificity: [0,0,0,1], index: 1}; 6 | var spec2 = {specificity: [0,0,1,0], index: 2}; 7 | var spec3 = {specificity: [0,1,0,0], index: 3}; 8 | var spec4 = {specificity: [1,0,0,0], index: 4}; 9 | var spec5 = {specificity: [0,0,1,1], index: 2}; 10 | var spec6 = {specificity: [0,0,1,0], index: 6}; 11 | 12 | describe('specificitySort', function() { 13 | 14 | it('should sort by highest specificity', function() { 15 | var array1 = [spec0, spec1]; 16 | var array2 = [spec0, spec1, spec2]; 17 | var array3 = [spec0, spec1, spec2, spec3, spec4]; 18 | var array4 = [spec2, spec5]; 19 | var array5 = [spec2, spec6]; 20 | 21 | expect( array1.sort(specificitySort), 'array1 not sorted correctly' ).to.deep.equals([spec1, spec0]); 22 | expect( array2.sort(specificitySort), 'array2 not sorted correctly' ).to.deep.equals([spec2, spec1, spec0]); 23 | expect( array3.sort(specificitySort), 'array3 not sorted correctly' ).to.deep.equals([spec4, spec3, spec2, spec1, spec0]); 24 | 25 | // test that ties on specificity category will resolve by lower specificity category 26 | expect( array4.sort(specificitySort), 'array4 not sorted correctly' ).to.deep.equals([spec5, spec2]); 27 | 28 | // test that ties are resolved by highest index order 29 | expect( array5.sort(specificitySort), 'array5 not sorted correctly' ).to.deep.equals([spec6, spec2]); 30 | }) 31 | }); 32 | 33 | 34 | 35 | 36 | 37 | /** 38 | * compareSpecificity 39 | */ 40 | describe('compareSpecificity', function() { 41 | 42 | it('should return the higher specificity', function() { 43 | // we don't care that the objects are equal, only that they contain the correct data 44 | // so we'll use deep equals instead of equal 45 | expect( compareSpecificity(spec0.specificity, spec1.specificity) ).to.deep.equal(spec1.specificity); 46 | expect( compareSpecificity(spec4.specificity, spec1.specificity) ).to.deep.equal(spec4.specificity); 47 | expect( compareSpecificity(spec5.specificity, spec2.specificity) ).to.deep.equal(spec5.specificity); 48 | expect( compareSpecificity(spec6.specificity, spec2.specificity) ).to.deep.equal(spec2.specificity); 49 | }); 50 | }); 51 | 52 | 53 | 54 | 55 | 56 | /** 57 | * parseStyleSheets 58 | */ 59 | describe('parseStyleSheets', function() { 60 | 61 | it('should dispatch an event when all styleSheets have been parsed', function(done) { 62 | document.addEventListener('styleSheetsParsed', function() { 63 | done(); 64 | }); 65 | 66 | // this call will add any async styleSheets to the internal dictionary, so all tests 67 | // can now be synchronous 68 | parseStyleSheets(); 69 | }); 70 | 71 | it('should create elements with the attribute "data-style-computed"', function() { 72 | expect($('[data-style-computed]').length).to.be.above(0); 73 | }); 74 | 75 | it('should skip any styleSheets with the "data-style-skip" attribute', function() { 76 | expect($('.audit-results[data-style-computed]').length).to.equal(0); 77 | }); 78 | 79 | it('should add a computedStyles property to an element that is affected by a styleSheet', function() { 80 | var button = $('button:not([class])')[0]; 81 | 82 | expect(button.computedStyles).to.exist; 83 | }); 84 | 85 | it('should correctly parse a styleSheet element rule', function() { 86 | var button = $('button:not([class])')[0]; 87 | 88 | // only 1 rule affects this button 89 | expect(Object.keys(button.computedStyles).length).to.equal(1); 90 | 91 | // the rule is font-size 92 | expect(button.computedStyles['font-size']).to.exist; 93 | expect(button.computedStyles['font-size'].length).to.equal(1); 94 | 95 | // ensure rule has correct data 96 | var rule = button.computedStyles['font-size'][0]; 97 | expect(rule.value).to.equal('16px'); 98 | expect(rule.specificity).to.deep.equal([0,0,0,1]); 99 | expect(rule.selector).to.equal('button'); 100 | expect(rule.index).to.equal(2); 101 | }); 102 | 103 | it('should correctly order multiple styleSheet rules on the same element', function() { 104 | var button = $('.pattern-lib-btn:not(.btn-override)')[0]; 105 | 106 | // font-size is declared twice 107 | expect(button.computedStyles['font-size']).to.exist; 108 | expect(button.computedStyles['font-size'].length).to.equal(2); 109 | 110 | // ensure the first rule is from the pattern library 111 | var rule = button.computedStyles['font-size'][0]; 112 | 113 | expect(rule.value).to.equal('14px'); 114 | expect(rule.specificity).to.deep.equal([0,0,1,0]); 115 | expect(rule.selector).to.equal('.pattern-lib-btn'); 116 | expect(rule.index).to.equal(3); 117 | }); 118 | 119 | }); --------------------------------------------------------------------------------