├── .gitignore ├── Gruntfile.js ├── Recipe.min.js ├── cssUsage.src.js ├── license.txt ├── package.json ├── readme.md ├── src ├── crawl │ └── prepareTsv.js ├── cssShorthands.js ├── cssUsage.js ├── custom │ └── custom-run-template.js ├── fwkUsage.js ├── fwks │ ├── blueprint.js │ ├── bootstrap.js │ ├── dogfalo.js │ ├── grumby.js │ ├── inuit.js │ ├── lonelyGates.js │ └── modernizr.js ├── htmlUsage.js ├── init.js ├── lodash.js ├── patternUsage.js ├── patterns.js └── recipes │ ├── archive │ ├── app-manifest.js │ ├── browserDownloadUrls.js │ ├── editControls.js │ ├── experimentalWebgl.js │ ├── imgEdgeSearch.js │ ├── max-width-replaced-elems.js │ ├── mediaelements.js │ ├── metaviewport.js │ ├── padding-hack-flex-context.js │ ├── padding-hack.js │ ├── paymentrequest.js │ ├── recipe-template.js │ ├── unsupportedBrowser.js │ └── zstaticflex.js │ └── file-input.js └── tests ├── recipes ├── PaymentRequest.html ├── app-manifest.html ├── editControls.html ├── experimentalWebgl.html ├── file-input.html ├── max-width-replaced-elems.html ├── mediaelements.html ├── metaviewport.html ├── padding-hack-flex-context.html ├── padding-hack.html └── unsupportedBrowser.html ├── test-cases ├── counting-tests-1.html ├── counting-tests-2.html ├── counting-tests-3.html └── counting-tests-4.html ├── test-page-atrules ├── index.html ├── styles.css └── testfont.ttf ├── test-page ├── firefox-error.html ├── index.html ├── style.css ├── styles │ └── aboutNetError.css └── testfont.ttf └── unit-tests ├── index.html └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | results/* 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 29 | node_modules 30 | 31 | 32 | # Visual Studio Code 33 | .vs/* -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | // Project configuration. 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | concat: { 7 | src: { 8 | src: [ 9 | 'src/lodash.js', 10 | 'src/cssShorthands.js', 11 | 'src/fwks/*', 12 | 'src/fwkUsage.js', 13 | 'src/patterns.js', 14 | 'src/patternUsage.js', 15 | 'src/htmlUsage.js', 16 | 'src/cssUsage.js', 17 | 'src/recipes/*', 18 | 'src/crawl/prepareTsv.js', 19 | 'src/init.js' 20 | ], 21 | dest: 'cssUsage.src.js' 22 | } 23 | }, 24 | strip_code: { 25 | options: { 26 | patterns: [ 27 | /currentRowTemplate.push\(\'(css|dom|html)\'\);/g, 28 | /convertToTSV\(INSTRUMENTATION_RESULTS\[\'(css|dom|html)\'\]\);[\n\r]+\s+currentRowTemplate.pop\(\);/g 29 | ] 30 | }, 31 | your_target: { 32 | files: [ 33 | {src: 'cssUsage.src.js', dest: 'Recipe.min.js'} 34 | ] 35 | } 36 | } 37 | }); 38 | 39 | grunt.loadNpmTasks('grunt-contrib-concat'); 40 | grunt.loadNpmTasks('grunt-strip-code'); 41 | grunt.registerTask('default', ['concat', 'strip_code']); 42 | }; -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | CSS Usage 2 | Copyright (c) Microsoft Corporation 3 | All rights reserved. 4 | MIT License 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ""Software""), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ms-edge-css-usage", 3 | "main": "src/cssUsage.js", 4 | "version": "0.1.0", 5 | "scripts": { 6 | "build": "grunt" 7 | }, 8 | "devDependencies": { 9 | "babel-preset-es2015": "^6.0.15", 10 | "chai": "^3.0.0", 11 | "grunt": "^1.0.0", 12 | "grunt-babel": "^6.0.0", 13 | "grunt-contrib-concat": "^1.0.1", 14 | "grunt-contrib-uglify": "^2.1.0", 15 | "grunt-mocha": "^1.0.4", 16 | "grunt-strip-code": "^1.0.4", 17 | "http-server": "^0.8.0", 18 | "mocha": "^2.2.5", 19 | "requirejs": "^2.1.18" 20 | } 21 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | CSS Usage 2 | ========= 3 | 4 | Overview 5 | -------- 6 | 7 | This is a script that will iterate over the document.styleSheets object and determine what was actually used from the linked stylesheets. 8 | 9 | #### Why did you make this? 10 | 11 | In order to help in planning decisions, it can be useful to know which feature or bug fix will make the biggest impact. 12 | Up until this point we knew what CSS was declared but not necessarily what was actually used by the browser. 13 | 14 | What it does and doesn't do 15 | --------------------------- 16 | 17 | There are pros and cons to any approach you take to gather telemetry. 18 | This is not the only approach we will take and think it is a good starting point to providing our telemetry externally 19 | for partners, other UAs, standards bodies, and web developers. 20 | 21 | Contributing 22 | ------------ 23 | 24 | #### Getting Started 25 | 26 | We highly recommend any advancements that you can make to the script. We expect it to only get better with the community providing pull requests. 27 | To get started you'll need to install [node](https://nodejs.org/) and [git](http://www.git-scm.com/) then do the following: 28 | 29 | 1. Fork the repo (see this page for [help](https://help.github.com/articles/fork-a-repo/)) 30 | 2. Clone your repo 31 | 3. Change directories into the root of your cloned directory 32 | 4. Then type `npm install` 33 | 5. Finally build the application `grunt` 34 | 35 | #### Making a change 36 | 37 | 1. Make the change 38 | 2. Write a unit test under `\tests\unit-tests\test.js` for your change if it is necessary 39 | 3. Run the tests in Chrome/Firefox/Edge and ensure that they still pass 40 | 4. Submit a pull request 41 | 42 | #### Legal 43 | 44 | You will **need** to complete a [Contributor License Agreement (CLA)](https://cla.microsoft.com) before your pull request can be accepted. This agreement testifies that you are granting us permission to use the source code you are submitting, and that this work is being submitted under appropriate license that we can use it. The process is very simple as it just hooks into your Github account. Once we have received the signed CLA, we'll review the request. You will only need to do this once. 45 | 46 | #### Tips on getting a successful pull request 47 | 48 | **Usefulness** 49 | 50 | One very important question to ask yourself before you write any code is 51 | > Will anyone actually use this? 52 | 53 | There are many reasons for this: 54 | * We want this data to be useful to many different demographics (Standards Bodies, Browser Vendors, Web Developers, etc) 55 | * We don't want to provide too much data that the site becomes unusable 56 | * There is overhead on Microsoft to add the new data to the site in a meaningful and intutive way so every change will have to answer this question (so be prepared to defend it) 57 | 58 | ### Code of Conduct 59 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 60 | -------------------------------------------------------------------------------- /src/crawl/prepareTsv.js: -------------------------------------------------------------------------------- 1 | // 2 | // This file is only here to create the TSV 3 | // necessary to collect the data from the crawler 4 | // 5 | void function() { 6 | 7 | /* String hash function 8 | /* credits goes to http://erlycoder.com/49/javascript-hash-functions-to-convert-string-into-integer-hash- */ 9 | const hashCodeOf = (str) => { 10 | var hash = 5381; var char = 0; 11 | for (var i = 0; i < str.length; i++) { 12 | char = str.charCodeAt(i); 13 | hash = ((hash << 5) + hash) + char; 14 | } 15 | return hash; 16 | } 17 | 18 | var ua = navigator.userAgent; 19 | var uaName = ua.indexOf('Edge')>=0 ? 'EDGE' :ua.indexOf('Chrome')>=0 ? 'CHROME' : 'FIREFOX'; 20 | window.INSTRUMENTATION_RESULTS = { 21 | UA: uaName, 22 | UASTRING: ua, 23 | UASTRING_HASH: hashCodeOf(ua), 24 | URL: location.href, 25 | TIMESTAMP: Date.now(), 26 | css: {/* see CSSUsageResults */}, 27 | html: {/* see HtmlUsageResults */}, 28 | dom: {}, 29 | scripts: {/* "bootstrap.js": 1 */}, 30 | }; 31 | window.INSTRUMENTATION_RESULTS_TSV = []; 32 | 33 | /* make the script work in the context of a webview */ 34 | try { 35 | var console = window.console || (window.console={log:function(){},warn:function(){},error:function(){}}); 36 | console.unsafeLog = console.log; 37 | console.log = function() { 38 | try { 39 | this.unsafeLog.apply(this,arguments); 40 | } catch(ex) { 41 | // ignore 42 | } 43 | }; 44 | } catch (ex) { 45 | // we tried... 46 | } 47 | }(); 48 | 49 | window.onCSSUsageResults = function onCSSUsageResults(CSSUsageResults) { 50 | // Collect the results (css) 51 | INSTRUMENTATION_RESULTS.css = CSSUsageResults; 52 | INSTRUMENTATION_RESULTS.html = HtmlUsageResults; 53 | INSTRUMENTATION_RESULTS.recipe = RecipeResults; 54 | 55 | // Convert it to a more efficient format 56 | INSTRUMENTATION_RESULTS_TSV = convertToTSV(INSTRUMENTATION_RESULTS); 57 | 58 | // Remove tabs and new lines from the data 59 | for(var i = INSTRUMENTATION_RESULTS_TSV.length; i--;) { 60 | var row = INSTRUMENTATION_RESULTS_TSV[i]; 61 | for(var j = row.length; j--;) { 62 | row[j] = (''+row[j]).replace(/(\s|\r|\n)+/g, ' '); 63 | } 64 | } 65 | 66 | // Convert into one signle tsv file 67 | var tsvString = INSTRUMENTATION_RESULTS_TSV.map((row) => (row.join('\t'))).join('\n'); 68 | appendTSV(tsvString); 69 | 70 | // Add it to the document dom 71 | function appendTSV(content) { 72 | if(window.debugCSSUsage) console.log("Trying to append"); 73 | var output = document.createElement('script'); 74 | output.id = "css-usage-tsv-results"; 75 | output.textContent = tsvString; 76 | output.type = 'text/plain'; 77 | document.querySelector('head').appendChild(output); 78 | var successfulAppend = checkAppend(); 79 | } 80 | 81 | function checkAppend() { 82 | if(window.debugCSSUsage) if(window.debugCSSUsage) console.log("Checking append"); 83 | var elem = document.getElementById('css-usage-tsv-results'); 84 | if(elem === null) { 85 | if(window.debugCSSUsage) console.log("Element not appended"); 86 | if(window.debugCSSUsage) console.log("Trying to append again"); 87 | appendTSV(); 88 | } 89 | else { 90 | if(window.debugCSSUsage) console.log("Element successfully found"); 91 | } 92 | } 93 | 94 | /** convert the instrumentation results to a spreadsheet for analysis */ 95 | function convertToTSV(INSTRUMENTATION_RESULTS) { 96 | if(window.debugCSSUsage) console.log("Converting to TSV"); 97 | 98 | var VALUE_COLUMN = 4; 99 | var finishedRows = []; 100 | var currentRowTemplate = [ 101 | INSTRUMENTATION_RESULTS.UA, 102 | INSTRUMENTATION_RESULTS.UASTRING_HASH, 103 | INSTRUMENTATION_RESULTS.URL, 104 | INSTRUMENTATION_RESULTS.TIMESTAMP, 105 | 0 106 | ]; 107 | 108 | currentRowTemplate.push('ua'); 109 | convertToTSV({identifier: INSTRUMENTATION_RESULTS.UASTRING}); 110 | currentRowTemplate.pop(); 111 | 112 | currentRowTemplate.push('css'); 113 | convertToTSV(INSTRUMENTATION_RESULTS['css']); 114 | currentRowTemplate.pop(); 115 | 116 | currentRowTemplate.push('dom'); 117 | convertToTSV(INSTRUMENTATION_RESULTS['dom']); 118 | currentRowTemplate.pop(); 119 | 120 | currentRowTemplate.push('html'); 121 | convertToTSV(INSTRUMENTATION_RESULTS['html']); 122 | currentRowTemplate.pop(); 123 | 124 | currentRowTemplate.push('recipe'); 125 | convertToTSV(INSTRUMENTATION_RESULTS['recipe']); 126 | currentRowTemplate.pop(); 127 | 128 | var l = finishedRows[0].length; 129 | finishedRows.sort((a,b) => { 130 | for(var i = VALUE_COLUMN+1; ib[i]) return +1; 133 | } 134 | return 0; 135 | }); 136 | 137 | return finishedRows; 138 | 139 | /** helper function doing the actual conversion */ 140 | function convertToTSV(object) { 141 | if(object==null || object==undefined || typeof object == 'number' || typeof object == 'string') { 142 | finishedRows.push(new Row(currentRowTemplate, ''+object)); 143 | } else { 144 | for(var key in object) { 145 | if({}.hasOwnProperty.call(object,key)) { 146 | currentRowTemplate.push(key); 147 | convertToTSV(object[key]); 148 | currentRowTemplate.pop(); 149 | } 150 | } 151 | } 152 | } 153 | 154 | /** constructor for a row of our table */ 155 | function Row(currentRowTemplate, value) { 156 | 157 | // Initialize an empty row with enough columns 158 | var row = [ 159 | /*UANAME: edge */'', 160 | /*UASTRING: mozilla/5.0 (...) */'', 161 | /*URL: http://.../... */'', 162 | /*TIMESTAMP: 1445622257303 */'', 163 | /*VALUE: 0|1|... */'', 164 | /*DATATYPE: css|dom|html... */'', 165 | /*SUBTYPE: props|types|api|... */'', 166 | /*NAME: font-size|querySelector|... */'', 167 | /*CONTEXT: count|values|... */'', 168 | /*SUBCONTEXT: px|em|... */'', 169 | /*... */'', 170 | /*... */'', 171 | ]; 172 | 173 | // Copy the column values from the template 174 | for(var i = currentRowTemplate.length; i--;) { 175 | row[i] = currentRowTemplate[i]; 176 | } 177 | 178 | // Add the value to the row 179 | row[VALUE_COLUMN] = value; 180 | 181 | return row; 182 | } 183 | 184 | } 185 | }; -------------------------------------------------------------------------------- /src/cssShorthands.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Based on: 3 | * https://github.com/gilmoreorless/css-shorthand-properties 4 | * MIT Licensed: http://gilmoreorless.mit-license.org/ 5 | */ 6 | void function () { 7 | /** 8 | * Data collated from multiple W3C specs: http://www.w3.org/Style/CSS/current-work 9 | */ 10 | var shorthands = this.shorthandProperties = { 11 | 12 | // CSS 2.1: http://www.w3.org/TR/CSS2/propidx.html 13 | 'list-style': ['-type', '-position', '-image'], 14 | 'margin': ['-top', '-right', '-bottom', '-left'], 15 | 'outline': ['-width', '-style', '-color'], 16 | 'padding': ['-top', '-right', '-bottom', '-left'], 17 | 18 | // CSS Backgrounds and Borders Module Level 3: http://www.w3.org/TR/css3-background/ 19 | 'background': ['-image', '-position', '-size', '-repeat', '-origin', '-clip', '-attachment', '-color'], 20 | 'background-repeat': ['-x','-y'], 21 | 'background-position': ['-x','-y'], 22 | 'border': ['-width', '-style', '-color'], 23 | 'border-color': ['border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color'], 24 | 'border-style': ['border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style'], 25 | 'border-width': ['border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width'], 26 | 'border-top': ['-width', '-style', '-color'], 27 | 'border-right': ['-width', '-style', '-color'], 28 | 'border-bottom': ['-width', '-style', '-color'], 29 | 'border-left': ['-width', '-style', '-color'], 30 | 'border-radius': ['border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius'], 31 | 'border-image': ['-source', '-slice', '-width', '-outset', '-repeat'], 32 | 33 | // CSS Fonts Module Level 3: http://www.w3.org/TR/css3-fonts/ 34 | 'font': ['-style', '-variant', '-weight', '-stretch', '-size', 'line-height', '-family'], 35 | 'font-variant': ['-ligatures', '-alternates', '-caps', '-numeric', '-east-asian'], 36 | 37 | // CSS Masking Module Level 1: http://www.w3.org/TR/css-masking/ 38 | 'mask': ['-image', '-mode', '-position', '-size', '-repeat', '-origin', '-clip'], 39 | 'mask-border': ['-source', '-slice', '-width', '-outset', '-repeat', '-mode'], 40 | 41 | // CSS Multi-column Layout Module: http://www.w3.org/TR/css3-multicol/ 42 | 'columns': ['column-width', 'column-count'], 43 | 'column-rule': ['-width', '-style', '-color'], 44 | 45 | // CSS Speech Module: http://www.w3.org/TR/css3-speech/ 46 | 'cue': ['-before', '-after'], 47 | 'pause': ['-before', '-after'], 48 | 'rest': ['-before', '-after'], 49 | 50 | // CSS Text Decoration Module Level 3: http://www.w3.org/TR/css-text-decor-3/ 51 | 'text-decoration': ['-line', '-style', '-color'], 52 | 'text-emphasis': ['-style', '-color'], 53 | 54 | // CSS Animations (WD): http://www.w3.org/TR/css3-animations 55 | 'animation': ['-name', '-duration', '-timing-function', '-delay', '-iteration-count', '-direction', '-fill-mode', '-play-state'], 56 | 57 | // CSS Transitions (WD): http://www.w3.org/TR/css3-transitions/ 58 | 'transition': ['-property', '-duration', '-timing-function', '-delay'], 59 | 60 | // CSS Flexible Box Layout Module Level 1 (WD): http://www.w3.org/TR/css3-flexbox/ 61 | 'flex': ['-grow', '-shrink', '-basis'], 62 | 63 | // CSS Grid: https://drafts.csswg.org/css-grid/#grid-shorthand 64 | 'grid': ['-template', '-auto-flow', '-auto-rows','-auto-columns'], 65 | 'grid-template': ['-rows', '-columns', '-areas'], 66 | 67 | // Others: 68 | 'overflow': ['-x','-y','-style'], // https://drafts.csswg.org/css-overflow-3/ 69 | 70 | }; 71 | 72 | var expandCache = Object.create(null); 73 | var unexpandCache = Object.create(null); 74 | 75 | /** 76 | * Expand a shorthand property into an array of longhand properties which are set by it 77 | * @param {string} property CSS property name 78 | * @return {array} List of longhand properties, or an empty array if it's not a shorthand 79 | */ 80 | this.expand = function (property) { 81 | 82 | var result = expandCache[property]; 83 | if(result) { return result; } 84 | 85 | var prefixData = property.match(/^(-[a-zA-Z]+-)?(.*)$/); 86 | var prefix = prefixData[1]||'', prefixFreeProperty = prefixData[2]||''; 87 | if (!shorthands.hasOwnProperty(prefixFreeProperty)) { 88 | return []; 89 | } 90 | 91 | result = []; 92 | shorthands[prefixFreeProperty].forEach((p) => { 93 | var longhand = p[0] === '-' ? property + p : prefix + p; 94 | result.push(longhand); 95 | result.push.apply(result, this.expand(longhand)); 96 | }); 97 | 98 | return expandCache[property] = result; 99 | 100 | }; 101 | 102 | /** 103 | * Expand a longhand property into an array of shorthand which may set the value 104 | * @param {string} property CSS property name 105 | * @return {array} List of shorthand properties, or the original property if it's not a shorthand 106 | */ 107 | this.unexpand = function unexpand(property) { 108 | 109 | var result = unexpandCache[property]; 110 | if(result) { return result; } 111 | 112 | var prefixData = property.match(/^(-[a-zA-Z]+-)?(.*)$/); 113 | var prefix = prefixData[1]||'', prefixFreeProperty = prefixData[2]||''; 114 | 115 | result = []; 116 | for(var sh = 0; sh <= shorthands.length; sh++) { 117 | var shorthand = shorthands[sh]; 118 | if(this.expand(shorthand).indexOf(prefixFreeProperty) >= 0) { 119 | result.push(prefix+shorthand); 120 | result.push.apply(result,this.unexpand(prefix+shorthand)); 121 | } 122 | } 123 | 124 | return unexpandCache[property] = result; 125 | 126 | } 127 | 128 | }.call(window.CSSShorthands={}); 129 | 130 | -------------------------------------------------------------------------------- /src/cssUsage.js: -------------------------------------------------------------------------------- 1 | void function() { try { 2 | 3 | var _ = window.CSSUsageLodash; 4 | var map = _.map.bind(_); 5 | var mapInline = _.mapInline ? _.mapInline : map; 6 | var reduce = _.reduce.bind(_); 7 | var filter = _.filter.bind(_); 8 | 9 | var browserIsEdge = navigator.userAgent.indexOf('Edge')>=0; 10 | var browserIsFirefox = navigator.userAgent.indexOf('Firefox')>=0; 11 | 12 | // 13 | // Guards execution against invalid conditions 14 | // 15 | void function() { 16 | 17 | // Don't run in subframes for now 18 | if (top.location.href !== location.href) throw new Error("CSSUsage: the script doesn't run in frames for now"); 19 | 20 | // Don't run if already ran 21 | if (window.CSSUsage) throw new Error("CSSUsage: second execution attempted; only one run can be executed; if you specified parameters, check the right ones were chosen"); 22 | 23 | // Don't run if we don't have lodash 24 | if (!window.CSSUsageLodash) throw new Error("CSSUsage: missing CSSUsageLodash dependency"); 25 | 26 | if (!window.HtmlUsage) throw new Error("APIUsage: missing HtmlUsage dependancy"); 27 | 28 | // Do not allow buggy trim() to bother usage 29 | if((''+String.prototype.trim).indexOf("[native code]") == -1) { 30 | console.warn('Replaced custom trim function with something known to work. Might break website.'); 31 | String.prototype.trim = function() { 32 | return this.replace(/^\s+|\s+$/g, ''); 33 | } 34 | } 35 | 36 | }(); 37 | 38 | // 39 | // Prepare our global namespace 40 | // 41 | void function() { 42 | if(window.debugCSSUsage) console.log("STAGE: Building up namespace"); 43 | window.HtmlUsageResults = { 44 | // this will contain all of the HTML tags used on a page 45 | tags: {}, /* 46 | tags ~= [nodeName] */ 47 | 48 | // this will contain all of the attributes used on an HTML tag 49 | // and their values if they are in the whitelist 50 | attributes: {} /* 51 | attributes ~= { 52 | name: , // The name of the attribute 53 | tag: , // The tag that the attr was used on 54 | value: // The value of the attr 55 | } */ 56 | }; 57 | 58 | window.RecipeResults = {}; 59 | window.Recipes = { 60 | recipes: [] 61 | }; 62 | 63 | window.CSSUsage = {}; 64 | window.CSSUsageResults = { 65 | 66 | // this will contain the usage stats of various at-rules and rules 67 | types: [ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, ], /* 68 | types ~= { 69 | "unknown":0, //0 70 | "style":0, //1 71 | "charset": 0, //2 72 | "import":0, //3 73 | "media":0, //4 74 | "font-face":0, //5 75 | "page":0, //6 76 | "keyframes":0, //7 This is the @keyframe at rule 77 | "keyframe":0, //8 This is the individual 0%, or from/to 78 | "reserved9":0, //9 79 | "namespace":0, //10 80 | "reserved11":0,//11 81 | "supports":0, //12 82 | "reserved13":0,//13 83 | "reserved14":0,//14 84 | "viewport":0, //15 85 | }*/ 86 | 87 | // this will contain the usage stats of various css properties and values 88 | props: Object.create(null), /* 89 | props ~= { 90 | "background-color": { 91 | count: 10, 92 | values: { 93 | "": 9, 94 | "inherit": 1 95 | } 96 | } 97 | }*/ 98 | 99 | // this will contains the various datapoints we measure on css selector usage 100 | usages: {"SuccessfulCrawls":1}, 101 | 102 | // this will contain selectors and the properties they refer to 103 | rules: {"@stylerule":0,"@atrule":0,"@inline":0}, /* 104 | rules ~= { 105 | "#id:hover .class": { 106 | count: 10, 107 | props: { 108 | "background-color": 5, 109 | "color": 4, 110 | "opacity": 3, 111 | "transform": 3 112 | } 113 | } 114 | }*/ 115 | 116 | atrules: {}/* 117 | atrules ~= { 118 | "@atrule:4": { 119 | count: 3, 120 | props: { 121 | "background-color": 1, 122 | "color": 4, 123 | "opacity": 3, 124 | "transform": 3 125 | }, 126 | nested: { 127 | "h3": 1 128 | }, 129 | conditions: { 130 | "screen": 1 131 | } 132 | } 133 | }*/ 134 | 135 | 136 | } 137 | }(); 138 | 139 | // 140 | // The StyleWalker API cover the extraction of style in the browser 141 | // 142 | void function() { "use strict"; 143 | 144 | CSSUsage.StyleWalker = { 145 | 146 | // This array contains the list of functions being run on each CSSStyleDeclaration 147 | // [ function(style, selectorText, matchedElements, ruleType) { ... }, ... ] 148 | ruleAnalyzers: [], 149 | 150 | // This array contains the list of functions being run on each DOM element of the page 151 | // [ function(element) { ...} ] 152 | elementAnalyzers: [], 153 | 154 | recipesToRun: [], 155 | runRecipes: false, 156 | 157 | // 158 | walkOverCssStyles: walkOverCssStyles, 159 | walkOverDomElements: walkOverDomElements, 160 | 161 | // Those stats are being collected while walking over the css style rules 162 | amountOfInlineStyles: 0, 163 | amountOfSelectorsUnused: 0, 164 | amountOfSelectors: 0, 165 | } 166 | 167 | var hasWalkedDomElementsOnce = false; 168 | // holds @keyframes temporarily while we wait to know how much they are used 169 | var keyframes = Object.create(null); 170 | 171 | /** 172 | * For all stylesheets of the document, 173 | * walk through the stylerules and run analyzers 174 | */ 175 | function walkOverCssStyles() { 176 | if(window.debugCSSUsage) console.log("STAGE: Walking over styles"); 177 | var styleSheets = document.styleSheets; 178 | 179 | // Loop through StyeSheets 180 | for (var ssIndex = styleSheets.length; ssIndex--;) { 181 | var styleSheet = styleSheets[ssIndex]; 182 | try { 183 | if(styleSheet.cssRules) { 184 | walkOverCssRules(styleSheet.cssRules, styleSheet); 185 | } else { 186 | console.warn("No content loaded for stylesheet: ", styleSheet.href||styleSheet); 187 | } 188 | } 189 | catch (e) { 190 | if(window.debugCSSUsage) console.log(e, e.stack); 191 | } 192 | } 193 | 194 | // Hack: rely on the results to find out which 195 | // animations actually run, and parse their keyframes 196 | var animations = (CSSUsageResults.props['animation-name']||{}).values||{}; 197 | for(var animation in keyframes) { 198 | var keyframe = keyframes[animation]; 199 | var matchCount = animations[animation]|0; 200 | var fakeElements = initArray(matchCount, (i)=>({tagName:'@keyframes '+animation+' ['+i+']'})); 201 | processRule(keyframe, fakeElements); 202 | } 203 | 204 | } 205 | 206 | /** 207 | * This is the css work horse, this will will loop over the 208 | * rules and then call the rule analyzers currently registered 209 | */ 210 | function walkOverCssRules(/*CSSRuleList*/ cssRules, styleSheet, parentMatchedElements) { 211 | if(window.debugCSSUsage) console.log("STAGE: Walking over rules"); 212 | for (var ruleIndex = cssRules.length; ruleIndex--;) { 213 | 214 | // Loop through the rules 215 | var rule = cssRules[ruleIndex]; 216 | 217 | // Until we can correlate animation usage 218 | // to keyframes do not parse @keyframe rules 219 | if(rule.type == 7) { 220 | keyframes[rule.name] = rule; 221 | continue; 222 | } 223 | 224 | // Filter "@supports" which the current browser doesn't support 225 | if(rule.type == 12 && (!CSS.supports || !CSS.supports(rule.conditionText))) { 226 | continue; 227 | } 228 | 229 | // Other rules should be processed immediately 230 | processRule(rule,parentMatchedElements); 231 | } 232 | } 233 | 234 | 235 | /** 236 | * This function takes a css rule and: 237 | * [1] walk over its child rules if needed 238 | * [2] call rule analyzers for that rule if it has style data 239 | */ 240 | function processRule(rule, parentMatchedElements) { 241 | // Increment the rule type's counter 242 | CSSUsageResults.types[rule.type|0]++; 243 | 244 | // Some CssRules have nested rules to walk through: 245 | if (rule.cssRules && rule.cssRules.length>0) { 246 | 247 | walkOverCssRules(rule.cssRules, rule.parentStyleSheet, parentMatchedElements); 248 | 249 | } 250 | 251 | // Some CssRules have style we can analyze 252 | if(rule.style) { 253 | // find what the rule applies to 254 | var selectorText; 255 | var matchedElements; 256 | if(rule.selectorText) { 257 | selectorText = CSSUsage.PropertyValuesAnalyzer.cleanSelectorText(rule.selectorText); 258 | try { 259 | if(parentMatchedElements) { 260 | matchedElements = [].slice.call(document.querySelectorAll(selectorText)); 261 | matchedElements.parentMatchedElements = parentMatchedElements; 262 | } else { 263 | matchedElements = [].slice.call(document.querySelectorAll(selectorText)); 264 | } 265 | } catch(ex) { 266 | matchedElements = []; 267 | console.warn(ex.stack||("Invalid selector: "+selectorText+" -- via "+rule.selectorText)); 268 | } 269 | } else { 270 | selectorText = '@atrule:'+rule.type; 271 | if(parentMatchedElements) { 272 | matchedElements = parentMatchedElements; 273 | } else { 274 | matchedElements = []; 275 | } 276 | } 277 | 278 | // run an analysis on it 279 | runRuleAnalyzers(rule.style, selectorText, matchedElements, rule.type); 280 | } 281 | 282 | // run analysis on at rules to populate CSSUsageResults.atrules 283 | if(isRuleAnAtRule(rule)) { 284 | if(rule.conditionText) { 285 | processConditionalAtRules(rule); 286 | } else { 287 | processGeneralAtRules(rule); 288 | } 289 | } 290 | } 291 | 292 | 293 | /** 294 | * Checks whether an rule is an @atrule. 295 | */ 296 | function isRuleAnAtRule(rule) { 297 | /** 298 | * @atrules types ~= { 299 | "charset": 0, //2 300 | "import":0, //3 301 | "media":0, //4 302 | "font-face":0, //5 303 | "page":0, //6 304 | "keyframes":0, //7 This is the @keyframe at rule 305 | "keyframe":0, //8 This is the individual 0%, or from/to 306 | 307 | "namespace":0, //10 308 | "supports":0, //12 309 | "viewport":0, //15 310 | */ 311 | let type = rule.type; 312 | return (type >= 2 && type <= 8) || (type == 10) || (type == 12) || (type == 15); 313 | } 314 | 315 | 316 | /** 317 | * This process @atrules with conditional statements such as @supports. 318 | * [1] It will process any props and values used within the body of the rule. 319 | * [2] It will count the occurence of usage of nested atrules. 320 | * [3] It will process condition statements to conform to a standardized version. 321 | */ 322 | function processConditionalAtRules(rule) { 323 | var selectorText = '@atrule:' + rule.type; 324 | var atrulesUsage = CSSUsageResults.atrules; 325 | 326 | if(!atrulesUsage[selectorText]) { 327 | atrulesUsage[selectorText] = Object.create(null); 328 | atrulesUsage[selectorText] = {"count": 1, 329 | "props": {}, 330 | "conditions": {}} 331 | } else { 332 | var count = atrulesUsage[selectorText].count; 333 | atrulesUsage[selectorText].count = count + 1; 334 | } 335 | 336 | var selectedAtruleUsage = atrulesUsage[selectorText]; 337 | 338 | if(rule.cssRules) { 339 | CSSUsage.PropertyValuesAnalyzer.anaylzeStyleOfRulePropCount(rule, selectedAtruleUsage); 340 | } 341 | 342 | processConditionText(rule.conditionText, selectedAtruleUsage.conditions); 343 | } 344 | 345 | /** 346 | * This processes the usage of conditions of conditional @atrules like @media. 347 | * Requires the condition of the rule to process and the current recorded usage 348 | * of the @atrule in question. 349 | */ 350 | function processConditionText(conditionText, selectedAtruleConditionalUsage) { 351 | // replace numeric specific information from condition statements 352 | conditionText = CSSUsage.CSSValues.parseValues(conditionText); 353 | 354 | if(!selectedAtruleConditionalUsage[conditionText]) { 355 | selectedAtruleConditionalUsage[conditionText] = Object.create(null); 356 | selectedAtruleConditionalUsage[conditionText] = {"count": 1} 357 | } else { 358 | var count = selectedAtruleConditionalUsage[conditionText].count; 359 | selectedAtruleConditionalUsage[conditionText].count = count + 1; 360 | } 361 | } 362 | 363 | /** 364 | * This will process all other @atrules that don't have conditions or styles. 365 | * [1] It will process any props and values used within the body of the rule. 366 | * [2] It will count the occurence of usage of nested atrules. 367 | */ 368 | function processGeneralAtRules(rule) { 369 | var selectorText = '@atrule:' + rule.type; 370 | var atrulesUsage = CSSUsageResults.atrules; 371 | 372 | if(!atrulesUsage[selectorText]) { 373 | atrulesUsage[selectorText] = Object.create(null); 374 | atrulesUsage[selectorText] = {"count": 1, 375 | "props": {}} 376 | } else { 377 | var count = atrulesUsage[selectorText].count; 378 | atrulesUsage[selectorText].count = count + 1; 379 | } 380 | 381 | // @keyframes rule type is 7 382 | if(rule.type == 7) { 383 | processKeyframeAtRules(rule); 384 | } else if(CSSUsageResults.rules[selectorText].props) { 385 | atrulesUsage[selectorText].props = CSSUsageResults.rules[selectorText].props; 386 | delete atrulesUsage[selectorText].props.values; 387 | } 388 | } 389 | 390 | /** 391 | * Processes on @keyframe to add the appropriate props from the frame and a counter of which 392 | * frames are used throughout the document. 393 | */ 394 | function processKeyframeAtRules(rule) { 395 | var selectorText = '@atrule:' + rule.type; 396 | var atrulesUsageForSelector = CSSUsageResults.atrules[selectorText]; 397 | 398 | if(!atrulesUsageForSelector["keyframes"]) { 399 | atrulesUsageForSelector["keyframes"] = Object.create(null); 400 | } 401 | 402 | /** 403 | * grab the props from the individual keyframe props that was already populated 404 | * under CSSUsageResults.rules. Note: @atrule:8 is the individual frames. 405 | * WARN: tightly coupled with previous processing of rules. 406 | */ 407 | atrulesUsageForSelector.props = CSSUsageResults.rules["@atrule:8"].props; 408 | delete atrulesUsageForSelector.props.values; 409 | 410 | for(let index in rule.cssRules) { 411 | let keyframe = rule.cssRules[index]; 412 | var atrulesUsageForKeyframeOfSelector = atrulesUsageForSelector.keyframes; 413 | 414 | if(!keyframe.keyText) { 415 | continue; 416 | } 417 | 418 | var frame = keyframe.keyText; 419 | 420 | // replace extra whitespaces 421 | frame = frame.replace(/\s/g, ''); 422 | 423 | if(!atrulesUsageForKeyframeOfSelector[frame]) { 424 | atrulesUsageForKeyframeOfSelector[frame] = { "count" : 1 }; 425 | } else { 426 | var keyframeCount = atrulesUsageForKeyframeOfSelector[frame].count; 427 | atrulesUsageForKeyframeOfSelector[frame].count = keyframeCount + 1; 428 | } 429 | } 430 | } 431 | 432 | 433 | /** 434 | * This is the dom work horse, this will will loop over the 435 | * dom elements and then call the element analyzers currently registered, 436 | * as well as rule analyzers for inline styles 437 | */ 438 | function walkOverDomElements(obj, index) { 439 | if(window.debugCSSUsage) console.log("STAGE: Walking over DOM elements"); 440 | var recipesToRun = CSSUsage.StyleWalker.recipesToRun; 441 | obj = obj || document.documentElement; index = index|0; 442 | 443 | // Loop through the elements 444 | var elements = [].slice.call(document.all,0); 445 | for(var i = 0; i < elements.length; i++) { 446 | var element=elements[i]; 447 | 448 | // Analyze its style, if any 449 | if(!CSSUsage.StyleWalker.runRecipes) { 450 | // Analyze the element 451 | runElementAnalyzers(element, index); 452 | 453 | if (element.hasAttribute('style')) { 454 | // Inline styles count like a style rule with no selector but one matched element 455 | var ruleType = 1; 456 | var isInline = true; 457 | var selectorText = '@inline:'+element.tagName; 458 | var matchedElements = [element]; 459 | runRuleAnalyzers(element.style, selectorText, matchedElements, ruleType, isInline); 460 | } 461 | } else { // We've already walked the DOM crawler and need to run the recipes 462 | for(var r = 0; r < recipesToRun.length ; r++) { 463 | var recipeToRun = recipesToRun[r]; 464 | var results = RecipeResults[recipeToRun.name] || (RecipeResults[recipeToRun.name]={}); 465 | recipeToRun(element, results, true); 466 | } 467 | } 468 | } 469 | 470 | } 471 | 472 | /** 473 | * Given a rule and its data, send it to all rule analyzers 474 | */ 475 | function runRuleAnalyzers(style, selectorText, matchedElements, type, isInline) { 476 | 477 | // Keep track of the counters 478 | if(isInline) { 479 | CSSUsage.StyleWalker.amountOfInlineStyles++; 480 | } else { 481 | CSSUsage.StyleWalker.amountOfSelectors++; 482 | } 483 | 484 | // Run all rule analyzers 485 | for(var i = 0; i < CSSUsage.StyleWalker.ruleAnalyzers.length; i++) { 486 | var runAnalyzer = CSSUsage.StyleWalker.ruleAnalyzers[i]; 487 | runAnalyzer(style, selectorText, matchedElements, type, isInline); 488 | } 489 | 490 | } 491 | 492 | /** 493 | * Given an element and its data, send it to all element analyzers 494 | */ 495 | function runElementAnalyzers(element, index, depth) { 496 | for(var i = 0; i < CSSUsage.StyleWalker.elementAnalyzers.length; i++) { 497 | var runAnalyzer = CSSUsage.StyleWalker.elementAnalyzers[i]; 498 | runAnalyzer(element, index, depth); 499 | } 500 | } 501 | 502 | /** 503 | * Creates an array of "length" elements, by calling initializer for each cell 504 | */ 505 | function initArray(length, initializer) { 506 | var array = Array(length); 507 | for(var i = length; i--;) { 508 | array[i] = initializer(i); 509 | } 510 | return array; 511 | } 512 | }(); 513 | 514 | // 515 | // helper to work with css values 516 | // 517 | void function() { 518 | 519 | CSSUsage.CSSValues = { 520 | createValueArray: createValueArray, 521 | parseValues: parseValues, 522 | normalizeValue: createValueArray 523 | }; 524 | 525 | /** 526 | * This will take a string value and reduce it down 527 | * to only the aspects of the value we wish to keep 528 | */ 529 | function parseValues(value,propertyName) { 530 | 531 | // Trim value on the edges 532 | value = value.trim(); 533 | 534 | // Normalize letter-casing 535 | value = value.toLowerCase(); 536 | 537 | // Map colors to a standard value (eg: white, blue, yellow) 538 | if (isKeywordColor(value)) { return ""; } 539 | value = value.replace(/[#][0-9a-fA-F]+/g, '#xxyyzz'); 540 | 541 | // Escapce identifiers containing numbers 542 | var numbers = ['ZERO','ONE','TWO','THREE','FOUR','FIVE','SIX','SEVEN','EIGHT','NINE']; 543 | value = value.replace( 544 | /([_a-z][-_a-z]|[_a-df-z])[0-9]+[-_a-z0-9]*/g, 545 | s=>numbers.reduce( 546 | (m,nstr,nint)=>m.replace(RegExp(nint,'g'),nstr), 547 | s 548 | ) 549 | ); 550 | 551 | // Remove any digits eg: 55px -> px, 1.5 -> 0.0, 1 -> 0 552 | value = value.replace(/(?:[+]|[-]|)(?:(?:[0-9]+)(?:[.][0-9]+|)|(?:[.][0-9]+))(?:[e](?:[+]|[-]|)(?:[0-9]+))?(%|e[a-z]+|[a-df-z][a-z]*)/g, "$1"); 553 | value = value.replace(/(?:[+]|[-]|)(?:[0-9]+)(?:[.][0-9]+)(?:[e](?:[+]|[-]|)(?:[0-9]+))?/g, " "); 554 | value = value.replace(/(?:[+]|[-]|)(?:[.][0-9]+)(?:[e](?:[+]|[-]|)(?:[0-9]+))?/g, " "); 555 | value = value.replace(/(?:[+]|[-]|)(?:[0-9]+)(?:[e](?:[+]|[-]|)(?:[0-9]+))/g, " "); 556 | value = value.replace(/(?:[+]|[-]|)(?:[0-9]+)/g, " "); 557 | 558 | // Unescapce identifiers containing numbers 559 | value = numbers.reduce( 560 | (m,nstr,nint)=>m.replace(RegExp(nstr,'g'),nint), 561 | value 562 | ) 563 | 564 | // Remove quotes 565 | value = value.replace(/('|‘|’|")/g, ""); 566 | 567 | // 568 | switch(propertyName) { 569 | case 'counter-increment': 570 | case 'counter-reset': 571 | 572 | // Anonymize the user identifier 573 | value = value.replace(/[-_a-zA-Z0-9]+/g,' '); 574 | break; 575 | 576 | case 'grid': 577 | case 'grid-template': 578 | case 'grid-template-rows': 579 | case 'grid-template-columns': 580 | case 'grid-template-areas': 581 | 582 | // Anonymize line names 583 | value = value.replace(/\[[-_a-zA-Z0-9 ]+\]/g,' '); 584 | break; 585 | 586 | case '--var': 587 | 588 | // Replace (...), {...} and [...] 589 | value = value.replace(/[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]*)[)])*[)])*[)])*[)])*[)]/g, " "); 590 | value = value.replace(/[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]*)[)])*[)])*[)])*[)])*[)]/g, " "); 591 | value = value.replace(/\[(?:[^()]+|\[(?:[^()]+|\[(?:[^()]+|\[(?:[^()]+|\[(?:[^()]*)\])*\])*\])*\])*\]/g, " "); 592 | value = value.replace(/\[(?:[^()]+|\[(?:[^()]+|\[(?:[^()]+|\[(?:[^()]+|\[(?:[^()]*)\])*\])*\])*\])*\]/g, " "); 593 | value = value.replace(/\{(?:[^()]+|\{(?:[^()]+|\{(?:[^()]+|\{(?:[^()]+|\{(?:[^()]*)\})*\})*\})*\})*\}/g, " "); 594 | value = value.replace(/\{(?:[^()]+|\{(?:[^()]+|\{(?:[^()]+|\{(?:[^()]+|\{(?:[^()]*)\})*\})*\})*\})*\}/g, " "); 595 | break; 596 | 597 | } 598 | 599 | return value.trim(); 600 | 601 | } 602 | 603 | //----------------------------------------------------------------------------- 604 | 605 | /** 606 | * This will transform a value into an array of value identifiers 607 | */ 608 | function createValueArray(value, propertyName, dontNormalize = false) { 609 | 610 | // Trim value on the edges 611 | value = value.trim(); 612 | 613 | // Normalize letter-casing 614 | value = value.toLowerCase(); 615 | 616 | // Do the right thing in function of the property 617 | if (!dontNormalize) { 618 | 619 | // Remove comments and !important 620 | value = value.replace(/([/][*](?:.|\r|\n)*[*][/]|[!]important.*)/g,''); 621 | 622 | switch (propertyName) { 623 | case 'font-family': 624 | 625 | // Remove various quotes 626 | if (value.indexOf("'") != -1 || value.indexOf("‘") != -1 || value.indexOf('"')) { 627 | value = value.replace(/('|‘|’|")/g, ""); 628 | } 629 | 630 | // Divide at commas to separate different font names 631 | value = value.split(/\s*,\s*/g); 632 | return value; 633 | 634 | case '--var': 635 | 636 | // Replace strings by dummies 637 | value = value.replace(/"([^"\\]|\\[^"\\]|\\\\|\\")*"/g,' ') 638 | value = value.replace(/'([^'\\]|\\[^'\\]|\\\\|\\')*'/g,' '); 639 | 640 | // Replace url(...) functions by dummies 641 | value = value.replace(/([a-z]?)[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]*)[)])*[)])*[)])*[)])*[)]/g, "$1()"); 642 | value = value.replace(/([a-z]?)[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]*)[)])*[)])*[)])*[)])*[)]/g, "$1()"); 643 | 644 | // Remove group contents (...), {...} and [...] 645 | value = value.replace(/[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]*)[)])*[)])*[)])*[)])*[)]/g, " "); 646 | value = value.replace(/[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]*)[)])*[)])*[)])*[)])*[)]/g, " "); 647 | value = value.replace(/[{](?:[^{}]+|[{](?:[^{}]+|[{](?:[^{}]+|[{](?:[^{}]+|[{](?:[^{}]*)[}])*[}])*[}])*[}])*[}]/g, " "); 648 | value = value.replace(/[{](?:[^{}]+|[{](?:[^{}]+|[{](?:[^{}]+|[{](?:[^{}]+|[{](?:[^{}]*)[}])*[}])*[}])*[}])*[}]/g, " "); 649 | value = value.replace(/[\[](?:[^\[\]]+|[\[](?:[^\[\]]+|[\[](?:[^\[\]]+|[\[](?:[^\[\]]+|[\[](?:[^\[\]]*)[\]])*[\]])*[\]])*[\]])*[\]]/g, " "); 650 | value = value.replace(/[\[](?:[^\[\]]+|[\[](?:[^\[\]]+|[\[](?:[^\[\]]+|[\[](?:[^\[\]]+|[\[](?:[^\[\]]*)[\]])*[\]])*[\]])*[\]])*[\]]/g, " "); 651 | 652 | break; 653 | 654 | default: 655 | 656 | // Replace strings by dummies 657 | value = value.replace(/"([^"\\]|\\[^"\\]|\\\\|\\")*"/g,' ') 658 | .replace(/'([^'\\]|\\[^'\\]|\\\\|\\')*'/g,' '); 659 | 660 | // Replace url(...) functions by dummies 661 | if (value.indexOf("(") != -1) { 662 | value = value.replace(/([a-z]?)[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]*)[)])*[)])*[)])*[)])*[)]/g, "$1() "); 663 | value = value.replace(/([a-z]?)[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]*)[)])*[)])*[)])*[)])*[)]/g, "$1() "); 664 | } 665 | 666 | } 667 | } 668 | else { 669 | switch (propertyName) { 670 | // If a URL value is given for the cursor property, then we want to remove all the speech marks put in around the url links 671 | // to prevent inconsistencies in results (in some running cases they are put in, others not) and split on the fallback values supplied. 672 | case 'cursor': 673 | 674 | // Remove various quotes - Crawler has some issue relating to speech marks, where sometimes they are put around url links (local test page run) and sometimes not (crawler run). 675 | if (value.indexOf("'") != -1 || value.indexOf("‘") != -1 || value.indexOf('"')) { 676 | value = value.replace(/('|‘|’|")/g, ""); 677 | } 678 | 679 | // Divide at commas to separate cursor url value and supplied fallback values. 680 | value = value.split(/\s*,\s*/g); 681 | return value; 682 | } 683 | } 684 | 685 | // Collapse whitespace 686 | value = value.trim().replace(/\s+/g, " "); 687 | 688 | // Divide at commas and spaces to separate different values 689 | value = value.split(/\s*(?:,|[/])\s*|\s+/g); 690 | 691 | return value; 692 | } 693 | 694 | /** 695 | * So that we don't end up with a ton of color 696 | * values, this will determine if the color is a 697 | * keyword color value 698 | */ 699 | function isKeywordColor(candidateColor) { 700 | 701 | // Keyword colors from the W3C specs 702 | var isColorKeyword = /^(aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgrey|darkgreen|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|gray|grey|green|greenyellow|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgreen|lightgray|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lighslategrey|lightsteelblue|lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|navyblue|oldlace|olive|olivedrab|orange|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|purple|rebeccapurple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|violet|wheat|white|whitesmoke|yellow|yellowgreen)$/; 703 | return isColorKeyword.test(candidateColor); 704 | 705 | } 706 | 707 | }(); 708 | 709 | // 710 | // computes various css stats (PropertyValuesAnalyzer) 711 | // 712 | void function() { 713 | 714 | CSSUsage.PropertyValuesAnalyzer = analyzeStyleOfRule; 715 | CSSUsage.PropertyValuesAnalyzer.cleanSelectorText = cleanSelectorText; 716 | CSSUsage.PropertyValuesAnalyzer.generalizedSelectorsOf = generalizedSelectorsOf; 717 | CSSUsage.PropertyValuesAnalyzer.finalize = finalize; 718 | CSSUsage.PropertyValuesAnalyzer.anaylzeStyleOfRulePropCount = anaylzeStyleOfRulePropCount; 719 | 720 | // We put a computed style in cache for filtering purposes 721 | var defaultStyle = getComputedStyle(document.createElement('div')); 722 | // As well as some basic lies 723 | var getBuggyValuesForThisBrowser = function() { 724 | var buggyValues = getBuggyValuesForThisBrowser.cache; 725 | if(buggyValues) { return buggyValues; } 726 | else { buggyValues = Object.create(null); } 727 | 728 | // Edge reports initial value instead of "initial", we have to be cautious 729 | if(browserIsEdge) { 730 | 731 | buggyValues['*'] = 1; // make 0 values automatic for longhand properties 732 | 733 | //buggyValues['list-style-position:outside'] = 0; 734 | buggyValues['list-style-image:none'] = 1; 735 | //buggyValues['outline-color:invert'] = 0; 736 | //buggyValues['outline-style:none'] = 0; 737 | //buggyValues['outline-width:medium'] = 0; 738 | //buggyValues['background-image:none'] = 0; 739 | //buggyValues['background-attachment:scroll'] = 0; 740 | //buggyValues['background-repeat:repeat'] = 0; 741 | //buggyValues['background-repeat-x:repeat'] = 0; 742 | //buggyValues['background-repeat-y:repeat'] = 0; 743 | //buggyValues['background-position-x:0%'] = 0; 744 | //buggyValues['background-position-y:0%'] = 0; 745 | //buggyValues['background-size:auto'] = 0; 746 | //buggyValues['background-origin:padding-box'] = 0; 747 | //buggyValues['background-clip:border-box'] = 0; 748 | //buggyValues['background-color:transparent'] = 0; 749 | buggyValues['border-top-color:currentcolor'] = 1; 750 | buggyValues['border-right-color:currentcolor'] = 1; 751 | buggyValues['border-bottom-color:currentcolor'] = 1; 752 | buggyValues['border-left-color:currentcolor'] = 1; 753 | //buggyValues['border-top-style:solid'] = 0; 754 | //buggyValues['border-right-style:solid'] = 0; 755 | //buggyValues['border-bottom-style:solid'] = 0; 756 | //buggyValues['border-left-style:solid'] = 0; 757 | buggyValues['border-top-width:medium'] = 1; 758 | buggyValues['border-right-width:medium'] = 1; 759 | buggyValues['border-bottom-width:medium'] = 1; 760 | buggyValues['border-left-width:medium'] = 1; 761 | buggyValues['border-image-source:none'] = 1; 762 | buggyValues['border-image-outset:0'] = 1; 763 | buggyValues['border-image-width:1'] = 1; 764 | buggyValues['border-image-repeat:repeat'] = 1; 765 | buggyValues['border-image-repeat-x:repeat'] = 1; 766 | buggyValues['border-image-repeat-y:repeat'] = 1; 767 | buggyValues['line-height:normal'] = 1; 768 | //buggyValues['font-size-adjust:none'] = 0; 769 | buggyValues['font-stretch:normal'] = 1; 770 | 771 | } 772 | 773 | // Firefox reports initial values instead of "initial", we have to be cautious 774 | if(browserIsFirefox) { 775 | 776 | buggyValues['*'] = 1; // make 0 values automatic for longhand properties 777 | 778 | } 779 | 780 | // Attempt to force to optimize the object somehow 781 | Object.create(buggyValues); 782 | 783 | return getBuggyValuesForThisBrowser.cache = buggyValues; 784 | 785 | }; 786 | var valueExistsInRootProperty = (cssText,key,rootKey,value) => { 787 | value = value.trim().toLowerCase(); 788 | 789 | // detect suspicious values 790 | var buggyValues = getBuggyValuesForThisBrowser(); 791 | 792 | // apply common sense to the given value, per browser 793 | var buggyState = buggyValues[key+':'+value]; 794 | if(buggyState === 1) { return false; } 795 | if(buggyState !== 0 && (!buggyValues['*'] || CSSShorthands.unexpand(key).length == 0)) { return true; } 796 | 797 | // root properties are unlikely to lie 798 | if(key==rootKey) return false; 799 | 800 | // ask the browser is the best we can do right now 801 | var values = value.split(/\s+|\s*,\s*/g); 802 | var validValues = ' '; 803 | var validValuesExtractor = new RegExp(' '+rootKey+'(?:[-][-_a-zA-Z0-9]+)?[:]([^;]*)','gi'); 804 | var match; while(match = validValuesExtractor.exec(cssText)) { 805 | validValues += match[1] + ' '; 806 | } 807 | for(var i = 0; i < values.length; i++) { 808 | var value = values[i]; 809 | if(validValues.indexOf(' '+value+' ')==-1) return false; 810 | } 811 | return true; 812 | 813 | }; 814 | 815 | /** This will loop over the styles declarations */ 816 | function analyzeStyleOfRule(style, selectorText, matchedElements, type, isInline) { isInline=!!isInline; 817 | 818 | // We want to filter rules that are not actually used 819 | var count = matchedElements.length; 820 | var selector = selectorText; 821 | var selectorCat = {'1:true':'@inline','1:false':'@stylerule'}[''+type+':'+isInline]||'@atrule'; 822 | 823 | // Keep track of unused rules 824 | var isRuleUnused = (count == 0); 825 | if(isRuleUnused) { 826 | CSSUsage.StyleWalker.amountOfSelectorsUnused++; 827 | } 828 | 829 | // We need a generalized selector to collect some stats 830 | var generalizedSelectors = ( 831 | (selectorCat=='@stylerule') 832 | ? [selectorCat].concat(generalizedSelectorsOf(selector)) 833 | : [selectorCat, selector] 834 | ); 835 | 836 | // Get the datastores of the generalized selectors 837 | var generalizedSelectorsData = map(generalizedSelectors, (generalizedSelector) => ( 838 | CSSUsageResults.rules[generalizedSelector] || (CSSUsageResults.rules[generalizedSelector] = {count:0,props:Object.create(null)}) 839 | )); 840 | 841 | // Increment the occurence counter of found generalized selectors 842 | for(var i = 0; i < generalizedSelectorsData.length; i++) { 843 | var generalizedSelectorData = generalizedSelectorsData[i]; 844 | generalizedSelectorData.count++ 845 | } 846 | 847 | // avoid most common browser lies 848 | var cssText = ' ' + style.cssText.toLowerCase(); 849 | if(browserIsEdge) { 850 | cssText = cssText.replace(/border: medium; border-image: none;/,'border: none;'); 851 | cssText = cssText.replace(/ border-image: none;/,' '); 852 | } 853 | 854 | // For each property declaration in this rule, we collect some stats 855 | for (var i = style.length; i--;) { 856 | 857 | var key = style[i], rootKeyIndex=key.indexOf('-'), rootKey = rootKeyIndex==-1 ? key : key.substr(0,rootKeyIndex); 858 | var normalizedKey = rootKeyIndex==0&&key.indexOf('-',1)==1 ? '--var' : key; 859 | var styleValue = style.getPropertyValue(key); 860 | 861 | // Only keep styles that were declared by the author 862 | // We need to make sure we're only checking string props 863 | var isValueInvalid = typeof styleValue !== 'string' && styleValue != "" && styleValue != undefined; 864 | if (isValueInvalid) { 865 | continue; 866 | } 867 | 868 | var isPropertyUndefined = (cssText.indexOf(' '+key+':') == -1) && (styleValue=='initial' || !valueExistsInRootProperty(cssText, key, rootKey, styleValue)); 869 | if (isPropertyUndefined) { 870 | continue; 871 | } 872 | 873 | // divide the value into simplified components 874 | var specifiedValuesArray = CSSUsage.CSSValues.createValueArray(styleValue,normalizedKey); 875 | var specifiedValuesUnnormalized = CSSUsage.CSSValues.createValueArray(styleValue,normalizedKey,true); 876 | var values = new Array(); 877 | for (var j = 0; j < specifiedValuesArray.length; ++j) { 878 | values.push(CSSUsage.CSSValues.parseValues(specifiedValuesArray[j],normalizedKey)); 879 | } 880 | 881 | // log the property usage per selector 882 | for(var gs = 0; gs < generalizedSelectorsData.length; gs++) { 883 | var generalizedSelectorData = generalizedSelectorsData[gs]; 884 | // get the datastore for current property 885 | var propStats = generalizedSelectorData.props[normalizedKey] || (generalizedSelectorData.props[normalizedKey] = {count:0,values:Object.create(null)}); 886 | 887 | // we saw the property one time 888 | propStats.count++; 889 | 890 | // we also saw a bunch of values 891 | for(var v = 0; v < values.length; v++) { 892 | var value = values[v]; 893 | // increment the counts for those by one, too 894 | if(value.length>0) { 895 | propStats.values[value] = (propStats.values[value]|0) + 1 896 | } 897 | 898 | } 899 | 900 | } 901 | 902 | // if we may increment some counts due to this declaration 903 | if(count > 0) { 904 | 905 | // instanciate or fetch the property metadata 906 | var propObject = CSSUsageResults.props[normalizedKey]; 907 | if (!propObject) { 908 | propObject = CSSUsageResults.props[normalizedKey] = { 909 | count: 0, 910 | values: Object.create(null) 911 | }; 912 | } 913 | 914 | // update the occurence counts of the property and value 915 | for(var e = 0; e < matchedElements.length; e++) { 916 | var element = matchedElements[e]; 917 | 918 | // check what the elements already contributed for this property 919 | var cssUsageMeta = element.CSSUsage || (element.CSSUsage=Object.create(null)); 920 | var knownValues = cssUsageMeta[normalizedKey] || (cssUsageMeta[normalizedKey] = []); 921 | 922 | // For recipes, at times we want to look at the specified values as well so hang 923 | // these on the element so we don't have to recompute them 924 | knownValues.valuesArray = knownValues.valuesArray || (knownValues.valuesArray = []); 925 | 926 | for(var sv = 0; sv < specifiedValuesUnnormalized.length; sv++) { 927 | var currentSV = specifiedValuesUnnormalized[sv]; 928 | if(knownValues.valuesArray.indexOf(currentSV) == -1) { 929 | knownValues.valuesArray.push(currentSV) 930 | } 931 | } 932 | 933 | // increment the amount of affected elements which we didn't count yet 934 | if(knownValues.length == 0) { propObject.count += 1; } 935 | 936 | // add newly found values too 937 | for(var v = 0; v < values.length; v++) { 938 | var value = values[v]; 939 | // Just want to keep the first of each distinct value for the CSS property. 940 | if (knownValues.indexOf(value) == -1) { 941 | propObject.values[value] = (propObject.values[value] | 0) + 1; 942 | knownValues.push(value); 943 | } 944 | } 945 | } 946 | } 947 | } 948 | } 949 | 950 | function anaylzeStyleOfRulePropCount(rule, selectedAtrulesUsage) { 951 | for(let index in rule.cssRules) { 952 | let ruleBody = rule.cssRules[index]; 953 | let style = ruleBody.style; 954 | 955 | // guard for non css objects 956 | if(!style) { 957 | continue; 958 | } 959 | 960 | if(ruleBody.selector) { 961 | try { 962 | var selectorText = CssPropertyValuesAnalyzer.cleanSelectorText(ruleBody.selectorText); 963 | var matchedElements = [].slice.call(document.querySelectorAll(selectorText)); 964 | 965 | if (matchedElements.length == 0) { 966 | continue; 967 | } 968 | } catch (ex) { 969 | console.warn(ex.stack||("Invalid selector: "+selectorText+" -- via "+ruleBody.selectorText)); 970 | continue; 971 | } 972 | } 973 | 974 | let cssText = ' ' + style.cssText.toLowerCase(); 975 | 976 | for (var i = style.length; i--;) { 977 | // processes out normalized prop name for style 978 | var key = style[i], rootKeyIndex=key.indexOf('-'), rootKey = rootKeyIndex==-1 ? key : key.substr(0,rootKeyIndex); 979 | var normalizedKey = rootKeyIndex==0&&key.indexOf('-',1)==1 ? '--var' : key; 980 | var styleValue = style.getPropertyValue(key); 981 | 982 | // Only keep styles that were declared by the author 983 | // We need to make sure we're only checking string props 984 | var isValueInvalid = typeof styleValue !== 'string' && styleValue != "" && styleValue != undefined; 985 | if (isValueInvalid) { 986 | continue; 987 | } 988 | 989 | var isPropertyUndefined = (cssText.indexOf(' '+key+':') == -1) && (styleValue=='initial' || !valueExistsInRootProperty(cssText, key, rootKey, styleValue)); 990 | if (isPropertyUndefined) { 991 | continue; 992 | } 993 | 994 | var propsForSelectedAtrule = selectedAtrulesUsage.props; 995 | 996 | if(!propsForSelectedAtrule[normalizedKey]) { 997 | propsForSelectedAtrule[normalizedKey] = Object.create(null); 998 | propsForSelectedAtrule[normalizedKey] = {"count": 1}; 999 | } else { 1000 | var propCount = propsForSelectedAtrule[normalizedKey].count; 1001 | propsForSelectedAtrule[normalizedKey].count = propCount + 1; 1002 | } 1003 | } 1004 | } 1005 | } 1006 | 1007 | function finalize() { 1008 | 1009 | // anonymize identifiers used for animation-name 1010 | function removeAnimationNames() { 1011 | 1012 | // anonymize identifiers used for animation-name globally 1013 | if(CSSUsageResults.props["animation-name"]) { 1014 | CSSUsageResults.props["animation-name"].values = {"":CSSUsageResults.props["animation-name"].count}; 1015 | } 1016 | 1017 | // anonymize identifiers used for animation-name per selector 1018 | for(var selector in CSSUsageResults.rules) { 1019 | var rule = CSSUsageResults.rules[selector]; 1020 | if(rule && rule.props && rule.props["animation-name"]) { 1021 | rule.props["animation-name"].values = {"":rule.props["animation-name"].count}; 1022 | } 1023 | } 1024 | 1025 | } 1026 | 1027 | removeAnimationNames(); 1028 | } 1029 | 1030 | //------------------------------------------------------------------------- 1031 | 1032 | /** 1033 | * If you try to do querySelectorAll on pseudo selectors 1034 | * it returns 0 because you are not actually doing the action the pseudo is stating those things, 1035 | * but we will honor those declarations and we don't want them to be missed, 1036 | * so we remove the pseudo selector from the selector text 1037 | */ 1038 | function cleanSelectorText(text) { 1039 | if(text.indexOf(':') == -1) { 1040 | return text; 1041 | } else { 1042 | return text.replace(/([-_a-zA-Z0-9*\[\]]?):(?:hover|active|focus|before|after|not\(:(hover|active|focus)\))|::(?:before|after)/gi, '>>$1<<').replace(/(^| |>|\+|~)>><><<\)/g,'(*)').replace(/>>([-_a-zA-Z0-9*\[\]]?)<a.submenu" => "#id.class:hover > a.class" 1049 | */ 1050 | function generalizedSelectorsOf(value) { 1051 | 1052 | // Trim 1053 | value = value.trim(); 1054 | 1055 | // Collapse whitespace 1056 | if (value) { 1057 | value = value.replace(/\s+/g, " "); 1058 | } 1059 | 1060 | // Remove (...) 1061 | if (value.indexOf("(") != -1) { 1062 | value = value.replace(/[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]*)[)])*[)])*[)])*[)])*[)]/g, ""); 1063 | value = value.replace(/[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]*)[)])*[)])*[)])*[)])*[)]/g, ""); 1064 | } 1065 | 1066 | // Simplify "..." and '...' 1067 | value = value.replace(/"([^"\\]|\\[^"\\]|\\\\|\\")*"/g,'""') 1068 | value = value.replace(/'([^'\\]|\\[^'\\]|\\\\|\\')*'/g,"''"); 1069 | 1070 | 1071 | // Simplify [att] 1072 | if (value.indexOf("[") != -1) { 1073 | value = value.replace(/\[[^=\[\]]+="([^"\\]|\\[^"\\]|\\\\|\\")*"\]/g, "[a]"); 1074 | value = value.replace(/\[[^=\[\]]+='([^'\\]|\\[^'\\]|\\\\|\\')*'\]/g, "[a]"); 1075 | value = value.replace(/\[[^\[\]]+\]/g, "[a]"); 1076 | } 1077 | 1078 | // Simplify .class 1079 | if (value.indexOf(".") != -1) { 1080 | value = value.replace(/[.][-_a-zA-Z][-_a-zA-Z0-9]*/g, ".c"); 1081 | } 1082 | 1083 | // Simplify #id 1084 | if (value.indexOf("#") != -1) { 1085 | value = value.replace(/[#][-_a-zA-Z][-_a-zA-Z0-9]*/g, "#i"); 1086 | } 1087 | 1088 | // Normalize combinators 1089 | value = value.replace(/[ ]*([>|+|~])[ ]*/g,' $1 '); 1090 | 1091 | // Trim whitespace 1092 | value = value.trim(); 1093 | 1094 | // Remove unnecessary * to match Chrome 1095 | value = value.replace(/[*]([#.\x5B:])/g,'$1'); 1096 | 1097 | // Now we can sort components so that all browsers give results similar to Chrome 1098 | value = sortSelectorComponents(value) 1099 | 1100 | // Split multiple selectors 1101 | value = value.split(/\s*,\s*/g); 1102 | 1103 | return value; 1104 | 1105 | } 1106 | 1107 | var ID_REGEXP = "[#]i"; // #id 1108 | var CLASS_REGEXP = "[.]c"; // .class 1109 | var ATTR_REGEXP = "\\[a\\]"; // [att] 1110 | var PSEUDO_REGEXP = "[:][:]?[-_a-zA-Z][-_a-zA-Z0-9]*"; // :pseudo 1111 | var SORT_REGEXPS = [ 1112 | 1113 | // #id first 1114 | new RegExp("("+CLASS_REGEXP+")("+ID_REGEXP+")",'g'), 1115 | new RegExp("("+ATTR_REGEXP+")("+ID_REGEXP+")",'g'), 1116 | new RegExp("("+PSEUDO_REGEXP+")("+ID_REGEXP+")",'g'), 1117 | 1118 | // .class second 1119 | new RegExp("("+ATTR_REGEXP+")("+CLASS_REGEXP+")",'g'), 1120 | new RegExp("("+PSEUDO_REGEXP+")("+CLASS_REGEXP+")",'g'), 1121 | 1122 | // [attr] third 1123 | new RegExp("("+PSEUDO_REGEXP+")("+ATTR_REGEXP+")",'g'), 1124 | 1125 | // :pseudo last 1126 | 1127 | ]; 1128 | function sortSelectorComponents(value) { 1129 | 1130 | var oldValue; do { // Yeah this is a very inefficient bubble sort. I know. 1131 | 1132 | oldValue = value; 1133 | for(var i = 0; i < SORT_REGEXPS.length; i++) { 1134 | var wrongPair = SORT_REGEXPS[i]; 1135 | value = value.replace(wrongPair,'$2$1'); 1136 | } 1137 | 1138 | } while(oldValue != value); return value; 1139 | 1140 | } 1141 | 1142 | }(); 1143 | 1144 | // 1145 | // extracts valuable informations about selectors in use 1146 | // 1147 | void function() { 1148 | 1149 | // 1150 | // To understand framework and general css usage, we collect stats about classes, ids and pseudos. 1151 | // Those objects have the following shape: 1152 | // {"hover":5,"active":1,"focus":2} 1153 | // 1154 | var cssPseudos = Object.create(null); // collect stats about which pseudo-classes and pseudo-elements are used in the css 1155 | var domClasses = Object.create(null); // collect stats about which css classes are found in the <... class> attributes of the dom 1156 | var cssClasses = Object.create(null); // collect stats about which css classes are used in the css 1157 | var domIds = Object.create(null); // collect stats about which ids are found in the <... id> attributes of the dom 1158 | var cssIds = Object.create(null); // collect stats about which ids are used in the css 1159 | 1160 | // 1161 | // To understand Modernizer usage, we need to know how often some classes are used at the front of a selector 1162 | // While we're at it, the code also collect the state for ids 1163 | // 1164 | var cssLonelyIdGates = Object.create(null); // .class something-else ==> {"class":1} 1165 | var cssLonelyClassGates = Object.create(null); // #id something-else ==> {"id":1} 1166 | var cssLonelyClassGatesMatches = []; // .class something-else ==> [".class something-else"] 1167 | var cssLonelyIdGatesMatches = []; // #id something-else ==> ["#id something-else"] 1168 | 1169 | // 1170 | // These regular expressions catch patterns we want to track (see before) 1171 | // 1172 | var ID_REGEXP = /[#][-_a-zA-Z][-_a-zA-Z0-9]*/g; // #id 1173 | var ID_REGEXP1 = /[#][-_a-zA-Z][-_a-zA-Z0-9]*/; // #id (only the first one) 1174 | var CLASS_REGEXP = /[.][-_a-zA-Z][-_a-zA-Z0-9]*/g; // .class 1175 | var CLASS_REGEXP1 = /[.][-_a-zA-Z][-_a-zA-Z0-9]*/; // .class (only the first one) 1176 | var PSEUDO_REGEXP = /[:][-_a-zA-Z][-_a-zA-Z0-9]*/g; // :pseudo (only the ) 1177 | var GATEID_REGEXP = /^\s*[#][-_a-zA-Z][-_a-zA-Z0-9]*([.][-_a-zA-Z][-_a-zA-Z0-9]*|[:][-_a-zA-Z][-_a-zA-Z0-9]*)*\s+[^>+{, ][^{,]+$/; // #id ... 1178 | var GATECLASS_REGEXP = /^\s*[.][-_a-zA-Z][-_a-zA-Z0-9]*([:][-_a-zA-Z][-_a-zA-Z0-9]*)*\s+[^>+{, ][^{,]+$/; // .class ... 1179 | 1180 | /** 1181 | * From a css selector text and a set of counters, 1182 | * increment the counters for the matches in the selector of the 'feature' regular expression 1183 | */ 1184 | function extractFeature(feature, selector, counters) { 1185 | var instances = selector.match(feature)||[]; 1186 | for(var i = 0; i < instances.length; i++) { 1187 | var instance = instances[i]; 1188 | instance = instance.substr(1); 1189 | counters[instance] = (counters[instance]|0) + 1; 1190 | } 1191 | } 1192 | 1193 | /** 1194 | * This analyzer will collect over the selectors the stats defined before 1195 | */ 1196 | CSSUsage.SelectorAnalyzer = function parseSelector(style, selectorsText) { 1197 | if(typeof selectorsText != 'string') return; 1198 | 1199 | var selectors = selectorsText.split(','); 1200 | for(var i = selectors.length; i--;) { var selector = selectors[i]; 1201 | 1202 | // extract all features from the selectors 1203 | extractFeature(ID_REGEXP, selector, cssIds); 1204 | extractFeature(CLASS_REGEXP, selector, cssClasses); 1205 | extractFeature(PSEUDO_REGEXP, selector, cssPseudos); 1206 | 1207 | // detect specific selector patterns we're interested in 1208 | if(GATEID_REGEXP.test(selector)) { 1209 | cssLonelyIdGatesMatches.push(selector); 1210 | extractFeature(ID_REGEXP1, selector, cssLonelyIdGates); 1211 | } 1212 | if(GATECLASS_REGEXP.test(selector)) { 1213 | cssLonelyClassGatesMatches.push(selector); 1214 | extractFeature(CLASS_REGEXP1, selector, cssLonelyClassGates); 1215 | } 1216 | } 1217 | 1218 | } 1219 | 1220 | /** 1221 | * This analyzer will collect over the dom elements the stats defined before 1222 | */ 1223 | CSSUsage.DOMClassAnalyzer = function(element) { 1224 | 1225 | // collect classes used in the wild 1226 | if(element.className) { 1227 | var elementClasses = element.classList; 1228 | for(var cl = 0; cl < elementClasses.length; cl++) { 1229 | var c = elementClasses[cl]; 1230 | domClasses[c] = (domClasses[c]|0) + 1; 1231 | } 1232 | } 1233 | 1234 | // collect ids used in the wild 1235 | if(element.id) { 1236 | domIds[element.id] = (domIds[element.id]|0) + 1; 1237 | } 1238 | 1239 | } 1240 | 1241 | /** 1242 | * This function will be called when all stats have been collected 1243 | * at which point we will agregate some of them in useful values like Bootstrap usages, etc... 1244 | */ 1245 | CSSUsage.SelectorAnalyzer.finalize = function() { 1246 | 1247 | // get arrays of the classes/ids used ({"hover":5} => ["hover"])) 1248 | var domClassesArray = Object.keys(domClasses); 1249 | var cssClassesArray = Object.keys(cssClasses); 1250 | var domIdsArray = Object.keys(domIds); 1251 | var cssIdsArray = Object.keys(cssIds); 1252 | 1253 | var results = { 1254 | // how many crawls are aggregated in this file (one of course in this case) 1255 | SuccessfulCrawls: 1, 1256 | 1257 | // how many elements on the page (used to compute percentages for css.props) 1258 | DOMElements: document.all.length, 1259 | 1260 | // how many selectors vs inline style, and other usage stats 1261 | SelectorsFound: CSSUsage.StyleWalker.amountOfSelectors, 1262 | InlineStylesFound: CSSUsage.StyleWalker.amountOfInlineStyles, 1263 | SelectorsUnused: CSSUsage.StyleWalker.amountOfSelectorsUnused, 1264 | 1265 | // ids stats 1266 | IdsUsed: domIdsArray.length, 1267 | IdsRecognized: Object.keys(cssIds).length, 1268 | IdsUsedRecognized: filter(domIdsArray, i => cssIds[i]).length, 1269 | 1270 | // classes stats 1271 | ClassesUsed: domClassesArray.length, 1272 | ClassesRecognized: Object.keys(cssClasses).length, 1273 | ClassesUsedRecognized: filter(domClassesArray, c => cssClasses[c]).length, 1274 | }; 1275 | 1276 | results = getFwkUsage(results, cssLonelyClassGates, domClasses, domIds, cssLonelyIdGates, cssClasses); 1277 | results = getPatternUsage(results, domClasses, cssClasses); 1278 | 1279 | CSSUsageResults.usages = results; 1280 | deleteDuplicatedAtRules(); // TODO: issue #52 1281 | 1282 | if(window.debugCSSUsage) if(window.debugCSSUsage) console.log(CSSUsageResults.usages); 1283 | } 1284 | 1285 | /** 1286 | * Removes duplicated at rules data that was generated under CSSUsageResults.rules 1287 | * TODO: should not be using such a function, refer to issue #52 1288 | */ 1289 | function deleteDuplicatedAtRules() { 1290 | var cssUsageRules = CSSUsageResults.rules; 1291 | var keys = Object.keys(cssUsageRules); 1292 | 1293 | for(let key of keys) { 1294 | // only remove specific atrules 1295 | if (key.includes("atrule:")) { 1296 | delete cssUsageRules[key]; 1297 | } 1298 | } 1299 | 1300 | delete CSSUsageResults.atrules["@atrule:8"]; // delete duplicated data from atrule:7, keyframe 1301 | } 1302 | }(); 1303 | 1304 | } catch (ex) { /* do something maybe */ throw ex; } }(); -------------------------------------------------------------------------------- /src/custom/custom-run-template.js: -------------------------------------------------------------------------------- 1 | void function() { 2 | document.addEventListener('DOMContentLoaded', function () { 3 | 4 | var results = ""; 5 | 6 | var title = document.getElementsByTagName('title')[0].textContent; 7 | results = title; 8 | appendResults(results); 9 | 10 | // Add it to the document dom 11 | function appendResults(results) { 12 | if(window.debugCSSUsage) console.log("Trying to append"); 13 | var output = document.createElement('script'); 14 | output.id = "css-usage-tsv-results"; 15 | output.textContent = results; 16 | output.type = 'text/plain'; 17 | document.querySelector('head').appendChild(output); 18 | var successfulAppend = checkAppend(); 19 | } 20 | 21 | function checkAppend() { 22 | if(window.debugCSSUsage) if(window.debugCSSUsage) console.log("Checking append"); 23 | var elem = document.getElementById('css-usage-tsv-results'); 24 | if(elem === null) { 25 | if(window.debugCSSUsage) console.log("Element not appended"); 26 | if(window.debugCSSUsage) console.log("Trying to append again"); 27 | appendTSV(); 28 | } 29 | else { 30 | if(window.debugCSSUsage) console.log("Element successfully found"); 31 | } 32 | } 33 | 34 | }); 35 | }(); -------------------------------------------------------------------------------- /src/fwkUsage.js: -------------------------------------------------------------------------------- 1 | function getFwkUsage(results, cssLonelyClassGates, domClasses, domIds, cssLonelyIdGates, cssClasses) { 2 | 3 | // Modernizer 4 | getLonelyGatesUsage(cssLonelyClassGates, domClasses, domIds, cssLonelyIdGates); 5 | detectedModernizerUsages(cssLonelyClassGates); 6 | results.FwkModernizer = !!window.Modernizer; 7 | results.FwkModernizerDOMUsages = detectedModernizerUsages(domClasses); 8 | results.FwkModernizerCSSUsages = detectedModernizerUsages(cssLonelyClassGates); 9 | 10 | // Bootstrap 11 | results.FwkBootstrap = !!((window.jQuery||window.$) && (window.jQuery||window.$).fn && (window.jQuery||window.$).fn.modal)|0; 12 | results.FwkBootstrapGridUsage = detectedBootstrapGridUsages(domClasses); 13 | results.FwkBootstrapFormUsage = detectedBootstrapFormUsages(domClasses); 14 | results.FwkBootstrapFloatUsage = detectedBootstrapFloatUsages(domClasses); 15 | results.FwkBootstrapAlertUsage = detectedBootstrapAlertUsages(domClasses); 16 | results.FwkBootstrapGridRecognized = detectedBootstrapGridUsages(cssClasses); 17 | results.FwkBootstrapFormRecognized = detectedBootstrapFormUsages(cssClasses); 18 | results.FwkBootstrapFloatRecognized = detectedBootstrapFloatUsages(cssClasses); 19 | results.FwkBootstrapAlertRecognized = detectedBootstrapAlertUsages(cssClasses); 20 | 21 | // Grumby 22 | results.FwkGrumby = hasGrumbyUsage()|0; 23 | 24 | // Inuit 25 | results.FwkInuit = hasInuitUsage()|0; 26 | 27 | // Blueprint 28 | results.FwkBluePrint = hasBluePrintUsage()|0; 29 | 30 | // Dog Falo 31 | results.FwkDogfaloMaterialize = hasDogfaloMaterializeUsage()|0; 32 | 33 | return results; 34 | } -------------------------------------------------------------------------------- /src/fwks/blueprint.js: -------------------------------------------------------------------------------- 1 | // http://blueprintcss.org/tests/parts/grid.html 2 | var hasBluePrintUsage = function() { 3 | 4 | if(!document.querySelector(".container")) { 5 | return false; 6 | } 7 | 8 | for(var i = 24+1; --i;) { 9 | if(document.querySelector(".container > .span-"+i)) { 10 | return true; 11 | } 12 | } 13 | return false; 14 | 15 | } -------------------------------------------------------------------------------- /src/fwks/bootstrap.js: -------------------------------------------------------------------------------- 1 | // 2 | // report how many times the classes in the following arrays have been used in the dom 3 | // (bootstrap stats) 4 | // 5 | 6 | var detectedBootstrapGridUsages = function(domClasses) { 7 | var _ = window.CSSUsageLodash; 8 | var reduce = _.reduce.bind(_); 9 | var trackedClasses = []; 10 | 11 | var sizes = ['xs','sm','md','lg']; 12 | for(var i = sizes.length; i--;) { var size = sizes[i]; 13 | for(var j = 12+1; --j;) { 14 | trackedClasses.push('col-'+size+'-'+j); 15 | for(var k = 12+1; --k;) { 16 | trackedClasses.push('col-'+size+'-'+j+'-offset-'+k); 17 | trackedClasses.push('col-'+size+'-'+j+'-push-'+k); 18 | trackedClasses.push('col-'+size+'-'+j+'-pull-'+k); 19 | } 20 | } 21 | } 22 | 23 | return reduce(trackedClasses, (a,b) => a+(domClasses[b]|0), 0); 24 | 25 | }; 26 | 27 | var detectedBootstrapFormUsages = function(domClasses) { 28 | var _ = window.CSSUsageLodash; 29 | var reduce = _.reduce.bind(_); 30 | var trackedClasses = [ 31 | 'form-group', 'form-group-xs', 'form-group-sm', 'form-group-md', 'form-group-lg', 32 | 'form-control', 'form-horizontal', 'form-inline', 33 | 'btn','btn-primary','btn-secondary','btn-success','btn-warning','btn-danger','btn-error' 34 | ]; 35 | 36 | return reduce(trackedClasses, (a,b) => a+(domClasses[b]|0), 0); 37 | 38 | }; 39 | 40 | var detectedBootstrapAlertUsages = function(domClasses) { 41 | var _ = window.CSSUsageLodash; 42 | var reduce = _.reduce.bind(_); 43 | var trackedClasses = [ 44 | 'alert','alert-primary','alert-secondary','alert-success','alert-warning','alert-danger','alert-error' 45 | ]; 46 | 47 | return reduce(trackedClasses, (a,b) => a+(domClasses[b]|0), 0); 48 | 49 | }; 50 | 51 | var detectedBootstrapFloatUsages = function(domClasses) { 52 | var _ = window.CSSUsageLodash; 53 | var reduce = _.reduce.bind(_); 54 | var trackedClasses = [ 55 | 'pull-left','pull-right', 56 | ]; 57 | 58 | return reduce(trackedClasses, (a,b) => a+(domClasses[b]|0), 0); 59 | 60 | }; -------------------------------------------------------------------------------- /src/fwks/dogfalo.js: -------------------------------------------------------------------------------- 1 | // https://github.com/Dogfalo/materialize/blob/master/sass/components/_grid.scss 2 | var hasDogfaloMaterializeUsage = function() { 3 | 4 | if(!document.querySelector(".container > .row > .col")) { 5 | return false; 6 | } 7 | 8 | for(var i = 12+1; --i;) { 9 | var classesToLookUp = ['s','m','l']; 10 | for(var d = 0; d < classesToLookUp.length; d++) { 11 | var s = classesToLookUp[d]; 12 | if(document.querySelector(".container > .row > .col."+s+""+i)) { 13 | return true; 14 | } 15 | } 16 | } 17 | return false; 18 | 19 | } -------------------------------------------------------------------------------- /src/fwks/grumby.js: -------------------------------------------------------------------------------- 1 | // http://www.gumbyframework.com/docs/grid/#!/basic-grid 2 | var hasGrumbyUsage = function() { 3 | 4 | if(!document.querySelector(".row .columns")) { 5 | return false; 6 | } 7 | 8 | var classesToLookUp = ["one","two","three","four","five","six","seven","eight","nine","ten","eleven","twelve"]; 9 | for(var cl = 0; cl < classesToLookUp.length; cl++ ) { 10 | var fraction = classesToLookUp[cl]; 11 | if(document.querySelector(".row > .columns."+fraction)) { 12 | return true; 13 | } 14 | } 15 | return false; 16 | 17 | } -------------------------------------------------------------------------------- /src/fwks/inuit.js: -------------------------------------------------------------------------------- 1 | // https://raw.githubusercontent.com/csswizardry/inuit.css/master/generic/_widths.scss 2 | var hasInuitUsage = function() { 3 | 4 | if(!document.querySelector(".grid .grid__item")) { 5 | return false; 6 | } 7 | 8 | var classesToLookUp = ["one-whole","one-half","one-third","two-thirds","one-quarter","two-quarters","one-half","three-quarters","one-fifth","two-fifths","three-fifths","four-fifths","one-sixth","two-sixths","one-third","three-sixths","one-half","four-sixths","two-thirds","five-sixths","one-eighth","two-eighths","one-quarter","three-eighths","four-eighths","one-half","five-eighths","six-eighths","three-quarters","seven-eighths","one-tenth","two-tenths","one-fifth","three-tenths","four-tenths","two-fifths","five-tenths","one-half","six-tenths","three-fifths","seven-tenths","eight-tenths","four-fifths","nine-tenths","one-twelfth","two-twelfths","one-sixth","three-twelfths","one-quarter","four-twelfths","one-third","five-twelfths","six-twelfths","one-half","seven-twelfths","eight-twelfths","two-thirds","nine-twelfths","three-quarters","ten-twelfths","five-sixths","eleven-twelfths"]; 9 | 10 | for(var cu = 0; cu < classesToLookUp.length; cu++ ) { 11 | var fraction = classesToLookUp[cu]; 12 | 13 | var subClassesToLookUp = ["","palm-","lap-","portable-","desk-"]; 14 | for(var sc = 0; sc < subClassesToLookUp.length; sc++) { 15 | var ns = subClassesToLookUp[sc]; 16 | if(document.querySelector(".grid > .grid__item."+ns+fraction)) { 17 | return true; 18 | } 19 | } 20 | } 21 | return false; 22 | 23 | } -------------------------------------------------------------------------------- /src/fwks/lonelyGates.js: -------------------------------------------------------------------------------- 1 | var getLonelyGatesUsage = function (cssLonelyClassGates, domClasses, domIds, cssLonelyIdGates) { 2 | 3 | var _ = window.CSSUsageLodash; 4 | 5 | if((cssLonelyClassGates || domClasses || domIds || cssLonelyIdGates) == undefined) return; 6 | 7 | // get arrays of the .class gates used ({"hover":5} => ["hover"]), filter irrelevant entries 8 | var cssUniqueLonelyClassGatesArray = Object.keys(cssLonelyClassGates); 9 | var cssUniqueLonelyClassGatesUsedArray = _(cssUniqueLonelyClassGatesArray).filter((c) => domClasses[c]).value(); 10 | var cssUniqueLonelyClassGatesUsedWorthArray = _(cssUniqueLonelyClassGatesUsedArray).filter((c)=>(cssLonelyClassGates[c]>9)).value(); 11 | if(window.debugCSSUsage) if(window.debugCSSUsage) console.log(cssLonelyClassGates); 12 | if(window.debugCSSUsage) if(window.debugCSSUsage) console.log(cssUniqueLonelyClassGatesUsedWorthArray); 13 | 14 | // get arrays of the #id gates used ({"hover":5} => ["hover"]), filter irrelevant entries 15 | var cssUniqueLonelyIdGatesArray = Object.keys(cssLonelyIdGates); 16 | var cssUniqueLonelyIdGatesUsedArray = _(cssUniqueLonelyIdGatesArray).filter((c) => domIds[c]).value(); 17 | var cssUniqueLonelyIdGatesUsedWorthArray = _(cssUniqueLonelyIdGatesUsedArray).filter((c)=>(cssLonelyIdGates[c]>9)).value(); 18 | if(window.debugCSSUsage) if(window.debugCSSUsage) console.log(cssLonelyIdGates); 19 | if(window.debugCSSUsage) if(window.debugCSSUsage) console.log(cssUniqueLonelyIdGatesUsedWorthArray); 20 | } -------------------------------------------------------------------------------- /src/fwks/modernizr.js: -------------------------------------------------------------------------------- 1 | // 2 | // report how many times the classes in the following arrays have been used as css gate 3 | // (modernizer stats) 4 | // 5 | 6 | // https://modernizr.com/docs#features 7 | var detectedModernizerUsages = function(cssLonelyClassGates) { 8 | 9 | if((cssLonelyClassGates) == undefined) return; 10 | 11 | var ModernizerUsages = {count:0,values:{/* "js":1, "no-js":2 */}}; 12 | var trackedClasses = ["js","ambientlight","applicationcache","audio","batteryapi","blobconstructor","canvas","canvastext","contenteditable","contextmenu","cookies","cors","cryptography","customprotocolhandler","customevent","dart","dataview","emoji","eventlistener","exiforientation","flash","fullscreen","gamepads","geolocation","hashchange","hiddenscroll","history","htmlimports","ie8compat","indexeddb","indexeddbblob","input","search","inputtypes","intl","json","olreversed","mathml","notification","pagevisibility","performance","pointerevents","pointerlock","postmessage","proximity","queryselector","quotamanagement","requestanimationframe","serviceworker","svg","templatestrings","touchevents","typedarrays","unicoderange","unicode","userdata","vibrate","video","vml","webintents","animation","webgl","websockets","xdomainrequest","adownload","audioloop","audiopreload","webaudio","lowbattery","canvasblending","todataurljpeg,todataurlpng,todataurlwebp","canvaswinding","getrandomvalues","cssall","cssanimations","appearance","backdropfilter","backgroundblendmode","backgroundcliptext","bgpositionshorthand","bgpositionxy","bgrepeatspace,bgrepeatround","backgroundsize","bgsizecover","borderimage","borderradius","boxshadow","boxsizing","csscalc","checked","csschunit","csscolumns","cubicbezierrange","display-runin","displaytable","ellipsis","cssescape","cssexunit","cssfilters","flexbox","flexboxlegacy","flexboxtweener","flexwrap","fontface","generatedcontent","cssgradients","hsla","csshyphens,softhyphens,softhyphensfind","cssinvalid","lastchild","cssmask","mediaqueries","multiplebgs","nthchild","objectfit","opacity","overflowscrolling","csspointerevents","csspositionsticky","csspseudoanimations","csspseudotransitions","cssreflections","regions","cssremunit","cssresize","rgba","cssscrollbar","shapes","siblinggeneral","subpixelfont","supports","target","textalignlast","textshadow","csstransforms","csstransforms3d","preserve3d","csstransitions","userselect","cssvalid","cssvhunit","cssvmaxunit","cssvminunit","cssvwunit","willchange","wrapflow","classlist","createelementattrs,createelement-attrs","dataset","documentfragment","hidden","microdata","mutationobserver","bdi","datalistelem","details","outputelem","picture","progressbar,meter","ruby","template","time","texttrackapi,track","unknownelements","es5array","es5date","es5function","es5object","es5","strictmode","es5string","es5syntax","es5undefined","es6array","contains","generators","es6math","es6number","es6object","promises","es6string","devicemotion,deviceorientation","oninput","filereader","filesystem","capture","fileinput","directory","formattribute","localizednumber","placeholder","requestautocomplete","formvalidation","sandbox","seamless","srcdoc","apng","jpeg2000","jpegxr","sizes","srcset","webpalpha","webpanimation","webplossless,webp-lossless","webp","inputformaction","inputformenctype","inputformmethod","inputformtarget","beacon","lowbandwidth","eventsource","fetch","xhrresponsetypearraybuffer","xhrresponsetypeblob","xhrresponsetypedocument","xhrresponsetypejson","xhrresponsetypetext","xhrresponsetype","xhr2","scriptasync","scriptdefer","speechrecognition","speechsynthesis","localstorage","sessionstorage","websqldatabase","stylescoped","svgasimg","svgclippaths","svgfilters","svgforeignobject","inlinesvg","smil","textareamaxlength","bloburls","datauri","urlparser","videoautoplay","videoloop","videopreload","webglextensions","datachannel","getusermedia","peerconnection","websocketsbinary","atob-btoa","framed","matchmedia","blobworkers","dataworkers","sharedworkers","transferables","webworkers"]; 13 | for(var tc = 0; tc < trackedClasses.length; tc++) { 14 | var c = trackedClasses[tc]; 15 | countInstancesOfTheClass(c); 16 | countInstancesOfTheClass('no-'+c); 17 | } 18 | return ModernizerUsages; 19 | 20 | function countInstancesOfTheClass(c) { 21 | var count = cssLonelyClassGates[c]; if(!count) return; 22 | ModernizerUsages.count += count; 23 | ModernizerUsages.values[c]=count; 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/htmlUsage.js: -------------------------------------------------------------------------------- 1 | void function() { 2 | 3 | window.HtmlUsage = {}; 4 | 5 | // This function has been added to the elementAnalyzers in 6 | // CSSUsage.js under onready() 7 | // is an HTMLElement passed in by elementAnalyzers 8 | window.HtmlUsage.GetNodeName = function (element) { 9 | 10 | // If the browser doesn't recognize the element - throw it away 11 | if(element instanceof HTMLUnknownElement) { 12 | return; 13 | } 14 | 15 | var node = element.nodeName; 16 | 17 | var tags = HtmlUsageResults.tags || (HtmlUsageResults.tags = {}); 18 | var tag = tags[node] || (tags[node] = 0); 19 | tags[node]++; 20 | 21 | GetAttributes(element, node); 22 | } 23 | 24 | function GetAttributes(element, node) { 25 | for(var i = 0; i < element.attributes.length; i++) { 26 | var att = element.attributes[i]; 27 | 28 | if(IsValidAttribute(element, att.nodeName)) { 29 | var attributes = HtmlUsageResults.attributes || (HtmlUsageResults.attributes = {}); 30 | var attribute = attributes[att.nodeName] || (attributes[att.nodeName] = {}); 31 | var attributeTag = attribute[node] || (attribute[node] = {count: 0}); 32 | attributeTag.count++; 33 | } 34 | } 35 | } 36 | 37 | function IsValidAttribute(element, attname) { 38 | // We need to convert className 39 | if(attname == "class") { 40 | attname = "className"; 41 | } 42 | 43 | if(attname == "classname") { 44 | return false; 45 | } 46 | 47 | // Only keep attributes that are not data 48 | if(attname.indexOf('data-') != -1) { 49 | return false; 50 | } 51 | 52 | if(typeof(element[attname]) == "undefined") { 53 | return false; 54 | } 55 | 56 | return true; 57 | } 58 | }(); -------------------------------------------------------------------------------- /src/init.js: -------------------------------------------------------------------------------- 1 | // 2 | // Execution scheduler: 3 | // This is where we decide what to run, and when 4 | // 5 | void function() { 6 | 7 | var browserIsEdge = navigator.userAgent.indexOf('Edge')>=0; 8 | var browserIsFirefox = navigator.userAgent.indexOf('Firefox')>=0; 9 | 10 | if(document.readyState !== 'complete') { 11 | 12 | // if the document is loading, run when it loads or in 10s, whichever is less 13 | window.addEventListener('load', onready); 14 | setTimeout(onready, 10000); 15 | 16 | } else { 17 | 18 | // if the document is ready, run now 19 | onready(); 20 | 21 | } 22 | 23 | /** 24 | * This is the main entrypoint of our script 25 | */ 26 | function onready() { 27 | 28 | // Uncomment if you want to set breakpoints when running in the console 29 | //debugger; 30 | 31 | // Prevent this code from running multiple times 32 | var firstTime = !onready.hasAlreadyRun; onready.hasAlreadyRun = true; 33 | if(!firstTime) { return; /* for now... */ } 34 | 35 | // Prevent this code from running when the page has no stylesheet (probably a redirect page) 36 | if(document.styleSheets.length == 0) { return; } 37 | 38 | // Check to see if you're on a Firefox failure page 39 | if(document.styleSheets.length == 1 && browserIsFirefox) { 40 | if(document.styleSheets[0].href !== null && document.styleSheets[0].href.indexOf('aboutNetError') != -1) { 41 | return; 42 | } 43 | } 44 | 45 | // Keep track of duration 46 | var startTime = performance.now(); 47 | 48 | // register tools 49 | CSSUsage.StyleWalker.ruleAnalyzers.push(CSSUsage.PropertyValuesAnalyzer); 50 | CSSUsage.StyleWalker.ruleAnalyzers.push(CSSUsage.SelectorAnalyzer); 51 | CSSUsage.StyleWalker.elementAnalyzers.push(CSSUsage.DOMClassAnalyzer); 52 | CSSUsage.StyleWalker.elementAnalyzers.push(HtmlUsage.GetNodeName); 53 | 54 | // perform analysis 55 | CSSUsage.StyleWalker.walkOverDomElements(); 56 | CSSUsage.StyleWalker.walkOverCssStyles(); 57 | CSSUsage.PropertyValuesAnalyzer.finalize(); 58 | CSSUsage.SelectorAnalyzer.finalize(); 59 | 60 | // Walk over the dom elements again for Recipes 61 | CSSUsage.StyleWalker.runRecipes = true; 62 | CSSUsage.StyleWalker.walkOverDomElements(); 63 | 64 | // Update duration 65 | CSSUsageResults.duration = (performance.now() - startTime)|0; 66 | 67 | // DO SOMETHING WITH THE CSS OBJECT HERE 68 | window.debugCSSUsage = false; 69 | if(window.onCSSUsageResults) { 70 | window.onCSSUsageResults(CSSUsageResults); 71 | } 72 | } 73 | }(); -------------------------------------------------------------------------------- /src/lodash.js: -------------------------------------------------------------------------------- 1 | void function() { 2 | 3 | var _ = (a => new ArrayWrapper(a)); 4 | _.mapInline = mapInline; 5 | _.map = map; /*.......*/ map.bind = (()=>map); 6 | _.filter = filter; /*.*/ filter.bind = (()=>filter); 7 | _.reduce = reduce; /*.*/ reduce.bind = (()=>reduce); 8 | window.CSSUsageLodash = _; 9 | // test case: 10 | // 35 = CSSUsageLodash([1,2,3,4,5]).map(v => v*v).filter(v => v%2).reduce(0, (a,b)=>(a+b)).value() 11 | 12 | function ArrayWrapper(array) { 13 | this.source = array; 14 | this.mapInline = function(f) { mapInline(this.source, f); return this; }; 15 | this.map = function(f) { this.source = map(this.source, f); return this; }; 16 | this.filter = function(f) { this.source = filter(this.source, f); return this; }; 17 | this.reduce = function(v,f) { this.source = reduce(this.source, f, v); return this; }; 18 | this.value = function() { return this.source }; 19 | } 20 | 21 | function map(source, transform) { 22 | var clone = new Array(source.length); 23 | for(var i = source.length; i--;) { 24 | clone[i] = transform(source[i]); 25 | } 26 | return clone; 27 | } 28 | 29 | function mapInline(source, transform) { 30 | for(var i = source.length; i--;) { 31 | source[i] = transform(source[i]); 32 | } 33 | return source; 34 | } 35 | 36 | function filter(source, shouldValueBeIncluded) { 37 | var clone = new Array(source.length), i=0; 38 | for(var s = 0; s <= source.length; s++) { 39 | var value = source[s]; 40 | if(shouldValueBeIncluded(value)) { 41 | clone[i++] = value 42 | } 43 | } 44 | clone.length = i; 45 | return clone; 46 | } 47 | 48 | function reduce(source, computeReduction, reduction) { 49 | for(var s = 0; s <= source.length; s++) { 50 | var value = source[s]; 51 | reduction = computeReduction(reduction, value); 52 | } 53 | return reduction; 54 | } 55 | 56 | }(); -------------------------------------------------------------------------------- /src/patternUsage.js: -------------------------------------------------------------------------------- 1 | function getPatternUsage(results, domClasses, cssClasses) { 2 | results.PatClearfixUsage = detectedClearfixUsages(domClasses); 3 | results.PatVisibilityUsage = detectedVisibilityUsages(domClasses); 4 | results.PatClearfixRecognized = detectedClearfixUsages(cssClasses); 5 | results.PatVisibilityRecognized = detectedVisibilityUsages(cssClasses); 6 | 7 | return results; 8 | } -------------------------------------------------------------------------------- /src/patterns.js: -------------------------------------------------------------------------------- 1 | // 2 | // report how many times the classes in the following arrays have been used in the dom 3 | // (general stats) 4 | // 5 | 6 | /** count how many times the usual clearfix classes are used */ 7 | var detectedClearfixUsages = function(domClasses) { 8 | 9 | var _ = window.CSSUsageLodash; 10 | var reduce = _.reduce.bind(_); 11 | 12 | var trackedClasses = [ 13 | 'clearfix','clear', 14 | ]; 15 | 16 | return reduce(trackedClasses, (a,b) => a+(domClasses[b]|0), 0); 17 | 18 | }; 19 | 20 | /** count how many times the usual hide/show classes are used */ 21 | var detectedVisibilityUsages = function(domClasses) { 22 | var _ = window.CSSUsageLodash; 23 | var reduce = _.reduce.bind(_); 24 | 25 | var trackedClasses = [ 26 | 'show', 'hide', 'visible', 'hidden', 27 | ]; 28 | 29 | return reduce(trackedClasses, (a,b) => a+(domClasses[b]|0), 0); 30 | 31 | }; -------------------------------------------------------------------------------- /src/recipes/archive/app-manifest.js: -------------------------------------------------------------------------------- 1 | /* 2 | RECIPE: APP MANIFEST 3 | ------------------------------------------------------------- 4 | Author: Joel Kucera 5 | Description: This recipe looks for app manifest declarations. 6 | ex, 7 | */ 8 | 9 | void function() { 10 | window.CSSUsage.StyleWalker.recipesToRun.push(function appManifest(element, results) { 11 | if(element.nodeName == 'LINK') { 12 | var relValue = element.getAttribute('rel'); 13 | if (relValue == 'manifest') 14 | { 15 | var value = element.getAttribute('href'); 16 | results[value] = results[value] || { count: 0 }; 17 | results[value].count++; 18 | } 19 | } 20 | 21 | return results; 22 | }); 23 | }(); -------------------------------------------------------------------------------- /src/recipes/archive/browserDownloadUrls.js: -------------------------------------------------------------------------------- 1 | /* 2 | RECIPE: browserDownloadUrls 3 | ------------------------------------------------------------- 4 | Author: Morgan, Lia, Joel, Malick 5 | Description: Looks for the download urls of other browsers 6 | */ 7 | 8 | 9 | void function() { 10 | window.CSSUsage.StyleWalker.recipesToRun.push( function browserDownloadUrls( element, results) { 11 | //tests for browser download urls 12 | var linkList = [{url:"https://www.google.com/chrome/", name:"Chrome"}, 13 | {url:"https://www.google.com/intl/en/chrome/browser/desktop/index.html", name:"Chrome"}, 14 | {url:"https://support.microsoft.com/en-us/help/17621/internet-explorer-downloads", name:"InternetExplorer"}, 15 | {url:"http://windows.microsoft.com/en-US/internet-explorer/downloads/ie", name:"InternetExplorer"}, 16 | {url:"https://www.mozilla.org/en-US/firefox/", name:"Firefox"}, 17 | {url:"https://www.apple.com/safari/", name:"Safari"}, 18 | {url:"https://support.apple.com/en-us/HT204416", name:"Safari"}, 19 | {url:"http://www.opera.com/download", name:"Opera"}, 20 | {url:"https://www.microsoft.com/en-us/download/details.aspx?id=48126", name:"Edge"}]; 21 | for(var j = 0; j < linkList.length; j++) { 22 | if(element.getAttribute("href") != null) { 23 | if(element.getAttribute("href").indexOf(linkList[j].url) != -1 ) { 24 | results[linkList[j].name] = results[linkList[j].name] || {count: 0}; 25 | results[linkList[j].name].count++; 26 | } 27 | } 28 | if (element.src != null) { 29 | if(element.src.indexOf(linkList[j].url) != -1 ) { 30 | results[linkList[j].name] = results[linkList[j].name] || {count: 0}; 31 | results[linkList[j].name].count++; 32 | } 33 | } 34 | } 35 | }); 36 | }(); -------------------------------------------------------------------------------- /src/recipes/archive/editControls.js: -------------------------------------------------------------------------------- 1 | /* 2 | RECIPE: Edit controls on the web 3 | ------------------------------------------------------------- 4 | Author: Grisha Lyukshin 5 | Description: Counts pages that have either input, textarea, or content editable elements 6 | */ 7 | 8 | void function() { 9 | window.CSSUsage.StyleWalker.recipesToRun.push( function editControls(/*HTML DOM Element*/ element, results) { 10 | 11 | // we only care about special kind of inputs 12 | if(element.nodeName.toLowerCase() === "input" && 13 | (element.getAttribute("type").toLowerCase() === "email" || 14 | element.getAttribute("type").toLowerCase() === "number" || 15 | element.getAttribute("type").toLowerCase() === "search" || 16 | element.getAttribute("type").toLowerCase() === "tel" || 17 | element.getAttribute("type").toLowerCase() === "url" || 18 | element.getAttribute("type").toLowerCase() === "text")) 19 | { 20 | results["input"] = results["input"] || { count: 0 }; 21 | results["input"].count++; 22 | } 23 | else if (element.nodeName.toLowerCase() === "textarea") 24 | { 25 | results["textarea"] = results["textarea"] || { count: 0 }; 26 | results["textarea"].count++; 27 | } 28 | else if (element.nodeName.toLowerCase() === "div" || element.nodeName.toLowerCase() === "p" || element.nodeName.toLowerCase() === "table") 29 | { 30 | if(element.getAttribute("contenteditable").toLowerCase() === "true" || element.getAttribute("contenteditable").toLowerCase() === "plain-text") 31 | { 32 | results["contenteditable"] = results["contenteditable"] || { count: 0 }; 33 | results["contenteditable"].count++; 34 | } 35 | } 36 | return results; 37 | }); 38 | }(); 39 | -------------------------------------------------------------------------------- /src/recipes/archive/experimentalWebgl.js: -------------------------------------------------------------------------------- 1 | /* 2 | RECIPE: Experimental WebGL 3 | ------------------------------------------------------------- 4 | Author: Mustapha Jaber 5 | Description: Find use of experimental-webgl in script. 6 | */ 7 | 8 | void function() { 9 | window.CSSUsage.StyleWalker.recipesToRun.push( function experimentalWebgl(/*HTML DOM Element*/ element, results) { 10 | var nodeName = element.nodeName; 11 | var script = "experimental-webgl" 12 | if (nodeName == "SCRIPT") 13 | { 14 | results[nodeName] = results[nodeName] || { count: 0, }; 15 | // if inline script. ensure that it's not our recipe script and look for string of interest 16 | if (element.text !== undefined && element.text.indexOf(script) != -1) 17 | { 18 | results[nodeName].count++; 19 | } 20 | else if (element.src !== undefined && element.src != "") 21 | { 22 | var xhr = new XMLHttpRequest(); 23 | xhr.open("GET", element.src, false); 24 | //xhr.setRequestHeader("Content-type", "text/javascript"); 25 | xhr.send(); 26 | if (xhr.status === 200 && xhr.responseText.indexOf(script) != -1) 27 | { 28 | results[nodeName].count++; 29 | } 30 | } 31 | } 32 | return results; 33 | }); 34 | }(); -------------------------------------------------------------------------------- /src/recipes/archive/imgEdgeSearch.js: -------------------------------------------------------------------------------- 1 | /* 2 | RECIPE: imgEdgeSearch 3 | ------------------------------------------------------------- 4 | Author: Morgan, Lia, Joel, Malick 5 | Description: Looking for sites that do not include edge as a supported browser 6 | */ 7 | 8 | void function() { 9 | window.CSSUsage.StyleWalker.recipesToRun.push( function imgEdgeSearch( element, results) { 10 | //tests for images 11 | if(element.nodeName == "IMG") { 12 | var browsers = ["internetexplorer","ie","firefox","chrome","safari","edge", "opera"]; 13 | for(var i = 0; i < browsers.length; i++) { 14 | if(element.getAttribute("alt").toLowerCase().indexOf(browsers[i]) != -1|| element.getAttribute("src").toLowerCase().indexOf(browsers[i]) != -1) { 15 | results[browsers[i]] = results[browsers[i]] || {count: 0, container: ""}; 16 | results[browsers[i]].count++; 17 | var parent = element.parentElement; 18 | 19 | if(parent) { 20 | var outer = element.parentElement.outerHTML; 21 | var val = outer.replace(element.parentElement.innerHTML, ""); 22 | results[browsers[i]].container = val; 23 | } 24 | } 25 | 26 | } 27 | } 28 | 29 | return results; 30 | }); 31 | }(); -------------------------------------------------------------------------------- /src/recipes/archive/max-width-replaced-elems.js: -------------------------------------------------------------------------------- 1 | /* 2 | RECIPE: Max-width on Replaced Elements 3 | ------------------------------------------------------------- 4 | Author: Greg Whitworth 5 | Description: This is investigation for the CSSWG looking into 6 | max-width with a % on a replaced element. If the results return 7 | too large we may want to take the next step to roughly determine 8 | if one of the parent's are depending on the sizing of its child. 9 | For example, abspos, table cell, floats, etc. That will be more 10 | computationally extensive so we'll start a simpler investigation first. 11 | 12 | Action Link: https://log.csswg.org/irc.w3.org/css/2017-03-01/#e778075 13 | */ 14 | 15 | void function() { 16 | window.CSSUsage.StyleWalker.recipesToRun.push( function MaxWidthPercentOnReplacedElem(element, results) { 17 | // Bail if the element doesn't have the props we're looking for 18 | if(!element.CSSUsage || !(element.CSSUsage["max-width"])) return; 19 | 20 | var replacedElems = ["INPUT", "TEXTAREA"]; 21 | var maxWidth = element.CSSUsage['max-width']; 22 | var width = element.CSSUsage['width']; 23 | 24 | if(!maxWidth.includes('%')) return; 25 | 26 | // We only want auto sized boxes 27 | if(width && !width.includes('auto')) return; 28 | 29 | if(replacedElems.includes(element.nodeName)) { 30 | 31 | if(element.nodeName == "INPUT" && element.type != "text") { 32 | return; 33 | } 34 | 35 | // TSV eg: 5 recipe MaxWidthPercentOnReplacedElem INPUT count 36 | results[element.nodeName] = results[element.nodeName] || { count: 0 }; 37 | results[element.nodeName].count++; 38 | } 39 | 40 | return results; 41 | }); 42 | }(); -------------------------------------------------------------------------------- /src/recipes/archive/mediaelements.js: -------------------------------------------------------------------------------- 1 | /* 2 | RECIPE: Metaviewport 3 | ------------------------------------------------------------- 4 | Author: Mustapha Jaber 5 | Description: Get count of media elements on page like video, audio, object. 6 | */ 7 | 8 | void function() { 9 | window.CSSUsage.StyleWalker.recipesToRun.push( function mediaelements(/*HTML DOM Element*/ element, results) { 10 | var nodeName = element.nodeName; 11 | if (nodeName == "OBJECT" || nodeName == "VIDEO" || nodeName == "AUDIO" || nodeName == "EMBED") 12 | { 13 | results[nodeName] = results[nodeName] || { count: 0, }; 14 | results[nodeName].count++; 15 | for (var n = 0; n < element.attributes.length; n++) { 16 | results[nodeName][element.attributes[n].name] = element.attributes[n].value; 17 | } 18 | } 19 | else if (IsAdobeFlashDownloadUrl(element)) 20 | { 21 | results[nodeName] = results[nodeName] || { flashDownloadUrl: true, count: 0 }; 22 | results[nodeName].count++; 23 | } 24 | return results; 25 | }); 26 | 27 | function IsAdobeFlashDownloadUrl(element) 28 | { 29 | var isFlashDownloadLink = false; 30 | 31 | if (element.nodeName == "A") 32 | { 33 | if (element.attributes["href"] !== undefined) { 34 | var host = element.attributes["href"].value; 35 | 36 | if (host.match("get.adobe.com") || host.match("get2.adobe.com")) { 37 | if (host.match("flash")) { 38 | // url is get.adobe.com/*flash* 39 | isFlashDownloadLink = true; 40 | } 41 | } 42 | else if (host.match("adobe.com") || host.match("macromedia.com") || 43 | host.match("www.adobe.com") || host.match("www.macromedia.com")) { 44 | var token = host.match("/go/"); 45 | if (token != null) { 46 | token = token.match("get"); 47 | if (token != null) { 48 | token = token.match("flash"); 49 | if (token != null) { 50 | // url is (www).adobe.com/*/go/*get*flash* or (www).macromedia.com/*/go/*get*flash* 51 | isFlashDownloadLink = true; 52 | } 53 | } 54 | } 55 | else if (host.match("/shockwave/download/") && 56 | host.match("download.cgi") || host.match("index.cgi")) { 57 | if (host.match("?P1_Prod_Version=ShockwaveFlash")) { 58 | // url is (www).(adobe || macromedia).com/shockwave/download/(download || index).cgi?P1_Prod_Version=ShockwaveFlash 59 | isFlashDownloadLink = true; 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | return isFlashDownloadLink; 67 | } 68 | }(); -------------------------------------------------------------------------------- /src/recipes/archive/metaviewport.js: -------------------------------------------------------------------------------- 1 | /* 2 | RECIPE: Metaviewport 3 | ------------------------------------------------------------- 4 | Author: Greg Whitworth 5 | Description: This will provide the values for the meta tag 6 | that also uses a content value with the values we're interested in. 7 | */ 8 | 9 | void function() { 10 | window.CSSUsage.StyleWalker.recipesToRun.push( function metaviewport(/*HTML DOM Element*/ element, results) { 11 | var needles = ["width", "height", "initial-scale", "minimum-scale", "maximum-scale", "user-scalable"]; 12 | 13 | if(element.nodeName == "META") { 14 | for(var n = 0; n < element.attributes.length; n++) { 15 | if(element.attributes[n].name == "content") { 16 | 17 | for(var needle = 0; needle < needles.length; needle++) { 18 | var value = element.attributes[n].value; 19 | 20 | if(value.indexOf(needles[needle]) != -1) { 21 | results[value] = results[value] || { count: 0 }; 22 | results[value].count++; 23 | break; 24 | } 25 | } 26 | } 27 | } 28 | } 29 | 30 | return results; 31 | }); 32 | }(); -------------------------------------------------------------------------------- /src/recipes/archive/padding-hack-flex-context.js: -------------------------------------------------------------------------------- 1 | /* 2 | RECIPE: PADDING HACK 3 | ------------------------------------------------------------- 4 | Author: Greg Whitworth 5 | Description: The padding hack is utilized in CSS by setting 6 | a bottom padding with a percentage value of great than 50% 7 | as this forces the box to set its height to that of the width 8 | and artificially creating aspect ratio based on its contents. 9 | 10 | This is a variant of the other padding hack recipe looking for 11 | % padding that is utilized on a flex item. 12 | */ 13 | 14 | void function() { 15 | window.CSSUsage.StyleWalker.recipesToRun.push(function paddingHackOnFlexItem(/*HTML DOM Element*/ element, results) { 16 | 17 | // Bail if the element doesn't have the props we're looking for 18 | if(!element.CSSUsage || !(element.CSSUsage["padding-bottom"] || element.CSSUsage["padding-top"])) return; 19 | 20 | // Bail if the element isn't a flex item 21 | var parent = element.parentNode; 22 | var display = window.getComputedStyle(parent).getPropertyValue("display"); 23 | if(display != "flex") return; 24 | 25 | var values = []; 26 | 27 | // Build up a stack of values to interrogate 28 | if(element.CSSUsage["padding-top"]) { 29 | values = values.concat(element.CSSUsage["padding-top"].valuesArray); 30 | } 31 | 32 | if(element.CSSUsage["padding-bottom"]) { 33 | values = values.concat(element.CSSUsage["padding-bottom"].valuesArray); 34 | } 35 | 36 | for(var i = 0; i < values.length; i++) { 37 | if(values[i].indexOf('%') != -1) { 38 | var value = values[i].replace('%', ""); 39 | value = parseFloat(value); 40 | 41 | if(value > 50) { 42 | results[value] = results[value] || { count: 0 }; 43 | results[value].count++; 44 | } 45 | } 46 | } 47 | 48 | return results; 49 | }); 50 | }(); -------------------------------------------------------------------------------- /src/recipes/archive/padding-hack.js: -------------------------------------------------------------------------------- 1 | /* 2 | RECIPE: PADDING HACK 3 | ------------------------------------------------------------- 4 | Author: Greg Whitworth 5 | Description: The padding hack is utilized in CSS by setting 6 | a bottom padding with a percentage value of great than 50% 7 | as this forces the box to set its height to that of the width 8 | and artificially creating aspect ratio based on its contents. 9 | */ 10 | 11 | void function() { 12 | window.CSSUsage.StyleWalker.recipesToRun.push(function paddingHack(/*HTML DOM Element*/ element, results) { 13 | 14 | // Bail if the element doesn't have the props we're looking for 15 | if(!element.CSSUsage || !(element.CSSUsage["padding-bottom"] || element.CSSUsage["padding-top"])) return; 16 | 17 | var values = []; 18 | 19 | // Build up a stack of values to interrogate 20 | if(element.CSSUsage["padding-top"]) { 21 | values = values.concat(element.CSSUsage["padding-top"].valuesArray); 22 | } 23 | 24 | if(element.CSSUsage["padding-bottom"]) { 25 | values = values.concat(element.CSSUsage["padding-bottom"].valuesArray); 26 | } 27 | 28 | for(var i = 0; i < values.length; i++) { 29 | if(values[i].indexOf('%') != -1) { 30 | var value = values[i].replace('%', ""); 31 | value = parseFloat(value); 32 | 33 | if(value > 50) { 34 | results[value] = results[value] || { count: 0 }; 35 | results[value].count++; 36 | } 37 | } 38 | } 39 | 40 | return results; 41 | }); 42 | }(); -------------------------------------------------------------------------------- /src/recipes/archive/paymentrequest.js: -------------------------------------------------------------------------------- 1 | /* 2 | RECIPE: Payment Request 3 | ------------------------------------------------------------- 4 | Author: Stanley Hon 5 | Description: This counts any page that includes any script references to PaymentRequest 6 | */ 7 | 8 | void function() { 9 | window.CSSUsage.StyleWalker.recipesToRun.push( function paymentrequest(/*HTML DOM Element*/ element, results) { 10 | 11 | if(element.nodeName == "SCRIPT") { 12 | if (element.innerText.indexOf("PaymentRequest") != -1) { 13 | results["use"] = results["use"] || { count: 0 }; 14 | results["use"].count++; 15 | } 16 | } 17 | 18 | return results; 19 | }); 20 | }(); -------------------------------------------------------------------------------- /src/recipes/archive/recipe-template.js: -------------------------------------------------------------------------------- 1 | /* 2 | RECIPE: 3 | ------------------------------------------------------------- 4 | Author: 5 | Description: 6 | 7 | void function() { 8 | window.CSSUsage.StyleWalker.recipesToRun.push( function ( element, results) { 9 | return results; 10 | }); 11 | }(); 12 | 13 | */ -------------------------------------------------------------------------------- /src/recipes/archive/unsupportedBrowser.js: -------------------------------------------------------------------------------- 1 | /* 2 | RECIPE: unsupported browser 3 | ------------------------------------------------------------- 4 | Author: Morgan Graham, Lia Hiscock 5 | Description: Looking for phrases that tell users that Edge is not supported, or to switch browers. 6 | */ 7 | 8 | void function() { 9 | window.CSSUsage.StyleWalker.recipesToRun.push( function unsupportedBrowser( element, results) { 10 | //tests for phrases 11 | var switchPhraseString = new RegExp("((?:Switch to|Get|Download|Install)(?:\\w|\\s)+(?:Google|Chrome|Safari|firefox|Opera|Internet Explorer|IE))","i"); 12 | var supportedPhraseString = new RegExp("((?:browser|Edge)(?:\\w|\\s)+(?:isn't|not|no longer)(?:\\w|\\s)+(?:supported|compatible))", "i"); 13 | var needles = [{str:switchPhraseString, name:"switchPhrase"}, 14 | {str:supportedPhraseString, name:"supportedPhrase"}];; 15 | 16 | for(var i = 0; i < needles.length; i++) { 17 | var found = element.textContent.match(needles[i].str); 18 | if(found) { 19 | if(found.length > 0 && found !== (null || undefined)) { 20 | results[needles[i].name] = results[needles[i].name] || {count: 0, match: "", container: ""}; 21 | results[needles[i].name].count++; 22 | 23 | var parent = element.parentElement; 24 | if(parent) { 25 | var outer = element.parentElement.outerHTML; 26 | var val = outer.replace(element.parentElement.innerHTML, ""); 27 | results[needles[i].name].container = val; 28 | } 29 | 30 | found = remove(found, " "); 31 | results[needles[i].name].match = found.join(); 32 | } 33 | } 34 | } 35 | 36 | return results; 37 | }); 38 | 39 | function remove(array, element) { 40 | return array.filter(e => e !== element); 41 | } 42 | }(); 43 | -------------------------------------------------------------------------------- /src/recipes/archive/zstaticflex.js: -------------------------------------------------------------------------------- 1 | /* 2 | RECIPE: z-index on static flex items 3 | ------------------------------------------------------------- 4 | Author: Francois Remy 5 | Description: Get count of flex items who should create a stacking context but do not really 6 | */ 7 | 8 | void function() { 9 | 10 | window.CSSUsage.StyleWalker.recipesToRun.push( function zstaticflex(/*HTML DOM Element*/ element, results) { 11 | if(!element.parentElement) return; 12 | 13 | // the problem happens if the element is a flex item with static position and non-auto z-index 14 | if(getComputedStyle(element.parentElement).display != 'flex') return results; 15 | if(getComputedStyle(element).position != 'static') return results; 16 | if(getComputedStyle(element).zIndex != 'auto') { 17 | results.likely = 1; 18 | } 19 | 20 | // the problem might happen if z-index could ever be non-auto 21 | if(element.CSSUsage["z-index"] && element.CSSUsage["z-index"].valuesArray.length > 0) { 22 | results.possible = 1; 23 | } 24 | 25 | }); 26 | }(); 27 | -------------------------------------------------------------------------------- /src/recipes/file-input.js: -------------------------------------------------------------------------------- 1 | /* 2 | RECIPE: File Input Usage 3 | ------------------------------------------------------------- 4 | Author: Greg Whitworth 5 | */ 6 | 7 | void function() { 8 | window.CSSUsage.StyleWalker.recipesToRun.push( function fileInputUsage(/*HTML DOM Element*/ element, results) { 9 | if(element.nodeName == "INPUT") { 10 | if (element.attributes.length > 0) { 11 | for(var n = 0; n < element.attributes.length; n++) { 12 | if(element.attributes[n].name == "type") { 13 | if (element.attributes[n].value.toLowerCase() === "file") { 14 | results["file"] = results["file"] || { count: 0 }; 15 | results["file"].count++; 16 | } 17 | } 18 | } 19 | } 20 | } 21 | 22 | return results; 23 | }); 24 | }(); -------------------------------------------------------------------------------- /tests/recipes/PaymentRequest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 42 | PaymentRequest test 43 | 44 | 45 | 46 | This page pretends to load the PaymentRequest API from google 47 | 48 | 49 | -------------------------------------------------------------------------------- /tests/recipes/app-manifest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | App Manifest test 6 | 7 | 8 | 9 | 10 | 11 |

App Manifest recipe test

12 | 13 | -------------------------------------------------------------------------------- /tests/recipes/editControls.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | This page pretends to host different edit controls 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
some text in content editable div
24 | 25 |
some text in content editable plain-text div
26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/recipes/experimentalWebgl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Media elements test 5 | 6 | 13 | 14 | 15 | 16 |
17 |

Testing experimental WebGL usage in script

18 |
19 | 28 | 29 | -------------------------------------------------------------------------------- /tests/recipes/file-input.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | File Input test 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/recipes/max-width-replaced-elems.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Max Width on Replaced Elements using % 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/recipes/mediaelements.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Media elements test 5 | 6 | 13 | 14 | 15 | 16 |
17 |

Testing Media Elements

18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |

Insufficient Flash Player Requirements

26 |
27 |

Click below to download the latest version of Flash

28 |

29 | 30 | Get Adobe Flash Player 31 | 32 |

33 |
34 |
35 |
36 | 37 | -------------------------------------------------------------------------------- /tests/recipes/metaviewport.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Metaviewport test 10 | 11 | 12 | 13 | 14 |
15 |

Testing Viewport

16 |
17 | 18 | -------------------------------------------------------------------------------- /tests/recipes/padding-hack-flex-context.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | % Padding used in a flex context 6 | 7 | 8 | 17 | 18 | 19 |
20 |
One
21 |
22 | 23 | -------------------------------------------------------------------------------- /tests/recipes/padding-hack.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Padding Hack 8 | 25 | 26 | 27 |
One
28 |
Two
29 |
Three
30 |
Four
31 | 32 | -------------------------------------------------------------------------------- /tests/recipes/unsupportedBrowser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Unsupported Browsers 8 | 20 | 21 | 22 |

Supported Browsers

23 |
24 |

Your current browser is not fully supported.

25 |

Get Chrome. You should get this browser.

26 |
27 |

28 |
29 | firefox logo 30 | Internet Explorer 31 | Opera 32 |
33 | 34 | -------------------------------------------------------------------------------- /tests/test-cases/counting-tests-1.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 33 | 34 | 35 | 36 |
    37 |
  • A
  • 38 |
  • B
  • 39 |
  • C
  • 40 |
  • D
  • 41 |
  • E
  • 42 |
43 | 44 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /tests/test-cases/counting-tests-2.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 32 | 33 | 34 | 35 | REDBLUE 36 | 37 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /tests/test-cases/counting-tests-3.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 21 | 22 | 23 | 24 |
25 | 26 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /tests/test-cases/counting-tests-4.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 26 | 27 | 28 | 29 |
You should enable JavaScript
30 |
31 |
32 |
33 | 34 |
35 |
36 | 37 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /tests/test-page-atrules/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | At-rules CSS Usage Testing Page 7 | 8 | 9 | 10 | 11 |
12 |

Helloooooooo

13 |

Yoyoyoyoyo

14 |

World!!!

15 |

But what about this?

16 |
17 | 18 | -------------------------------------------------------------------------------- /tests/test-page-atrules/styles.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | @import url(http://fonts.googleapis.com/css?family=Source+Sans+Pro:400,900); 3 | 4 | @font-face { 5 | font-family: 'testfont'; 6 | src: url('testfont.ttf') format('truetype'); 7 | } 8 | 9 | @supports (display: flex) { 10 | #changeWhenSupported { 11 | color: orange; 12 | } 13 | } 14 | 15 | @supports (text-decoration: underline) { 16 | #changeColorWhenSupported { 17 | color: blue; 18 | } 19 | } 20 | 21 | @supports (display: grid) { 22 | #changeWhenSupported { 23 | text-align: center; 24 | } 25 | } 26 | 27 | @supports not (display: flex) { 28 | #changeWhenSupported { 29 | color: orchid; 30 | } 31 | } 32 | 33 | @keyframes hue { 34 | 0% { 35 | background-color: aliceblue; 36 | } 37 | 38 | 50% { 39 | background-color: powderblue; 40 | } 41 | 42 | 100% { 43 | background-color: aliceblue; 44 | } 45 | } 46 | 47 | .wrapper { 48 | animation: hue 8s infinite; 49 | } 50 | 51 | @keyframes pulse { 52 | from { 53 | color: #001f3f; 54 | margin-left: 0em; 55 | } 56 | 57 | to { 58 | color: #ff4136; 59 | margin-left: 10em; 60 | } 61 | } 62 | 63 | h2 { 64 | animation: pulse 5s infinite; 65 | } 66 | 67 | #changeWhenSupported { 68 | text-decoration: underline; 69 | } 70 | 71 | @media only screen and (min-device-width: 320px) { 72 | 73 | h3 { 74 | color: orchid; 75 | } 76 | 77 | } 78 | 79 | @media screen and (min-width:480px) { 80 | p { 81 | background-color: whitesmoke; 82 | } 83 | 84 | body{ 85 | background-color:#6aa6cc; 86 | color:#F7FFAD; 87 | } 88 | 89 | @media screen and (min-width: 768px) { 90 | 91 | body{ 92 | background-color:#FAC1E0; 93 | color:#fff; 94 | } 95 | } 96 | 97 | @media screen and (min-width: 1040px) { 98 | 99 | body{ 100 | background-color:#FAC1E0; 101 | color:#fff; 102 | } 103 | } 104 | } 105 | 106 | @page { 107 | margin: 1cm; 108 | } 109 | 110 | @page :first { 111 | margin: 2cm; 112 | } -------------------------------------------------------------------------------- /tests/test-page-atrules/testfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/css-usage/e2caffc6295bdd3a2aba61c2a7771924649c66eb/tests/test-page-atrules/testfont.ttf -------------------------------------------------------------------------------- /tests/test-page/firefox-error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | This is to test Firefox error page detection 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/test-page/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |

Header

15 |
16 | 23 |
24 |

Header using font-face

25 |
26 | 27 |
28 | 29 | 30 |

Random stuff to make sure that I cover values and props

31 |
Text
32 |

CRAZY!!!

33 |
Cursor test
34 |
35 |
36 | 37 |
VARIABLES
38 | 39 |
40 | 41 | -------------------------------------------------------------------------------- /tests/test-page/style.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Source+Sans+Pro:400,900); 2 | 3 | @font-face { 4 | font-family: 'testfont'; 5 | src: url('testfont.ttf') format('truetype'); 6 | } 7 | 8 | .wrapper { 9 | width: 80%; 10 | height: 80%; 11 | font-family: Arial; 12 | background: #cccccc; 13 | margin: auto; 14 | color: yellow; 15 | border: 1px solid #ffffff; 16 | } 17 | 18 | h1 { font-family: ‘Metrophobic’, Arial, serif; font-weight: 400; } 19 | 20 | .wrapper { 21 | color: black; 22 | } 23 | 24 | .wrapper, 25 | header, 26 | nav, 27 | article, 28 | aside, 29 | footer { 30 | border-radius: 5px; 31 | padding: 10px; 32 | } 33 | 34 | header { 35 | background: #6870DE; 36 | } 37 | 38 | header h1 { 39 | margin: 0; 40 | color: white; 41 | font-size: 25px; 42 | } 43 | 44 | nav { 45 | margin: 5px 0; 46 | background: #666; 47 | } 48 | 49 | nav ul { 50 | margin: 0; 51 | padding: 0; 52 | } 53 | 54 | nav ul li { 55 | display: inline-block; 56 | } 57 | 58 | nav ul li a { 59 | text-decoration: none; 60 | color: black; 61 | } 62 | 63 | nav ul li a { 64 | color: white; 65 | } 66 | 67 | article, aside { 68 | float: left; 69 | height: 300px; 70 | margin-bottom: 10px; 71 | } 72 | 73 | .testfont { 74 | font-family: 'testfont'; 75 | font-size: 4em; 76 | } 77 | 78 | article { 79 | width: 73%; 80 | margin-right: 2%; 81 | background: #eee; 82 | } 83 | 84 | aside { 85 | width: 20.5%; 86 | background: #ddd; 87 | } 88 | 89 | footer { 90 | clear: both; 91 | height: 25px; 92 | background: #666; 93 | } 94 | 95 | .box { 96 | width: 100%; 97 | height: 50px; 98 | background-color: rgba(200, 54, 54, 0.5); 99 | font-family: Baskerville, "Baskerville Old Face", "Hoefler Text", Garamond, "Times New Roman", serif; 100 | } 101 | 102 | .box:hover { 103 | transform: rotate(90deg); 104 | transition-delay: 1s; 105 | transition-duration: 1s; 106 | transform-style: flat; 107 | transition-property: all; 108 | background-color: hsl(120, 100%, 50%); 109 | color: hsla(120, 100%, 50%, .75); 110 | } 111 | 112 | .webkit-tests { 113 | background: -webkit-linear-gradient(red, blue); 114 | background: linear-gradient( 45deg, blue, red ); 115 | background: linear-gradient( 45deg, blue, red ) no-repeat; 116 | } 117 | 118 | .animation-test { 119 | animation-duration: 1s; 120 | animation-name: test; 121 | animation-iteration-count: 3; 122 | width: 10px; 123 | height: 10px; 124 | background: blue; 125 | position: absolute; 126 | top: 0; 127 | left: 0; 128 | } 129 | 130 | @keyframes test { 131 | from { 132 | margin-left: 0px; 133 | margin-top:0em; 134 | } 135 | 136 | to { 137 | margin-left: 75px; 138 | margin-top: 10em; 139 | } 140 | } 141 | 142 | @media all and (max-width: 699px) { 143 | .wrapper { 144 | width: 90%; 145 | } 146 | 147 | @keyframes test { 148 | from { 149 | margin-right: 5em; 150 | } 151 | 152 | to { 153 | margin-right: 300em; 154 | } 155 | } 156 | } 157 | 158 | @supports (display: flex) { 159 | body { 160 | font-size: 1em; 161 | } 162 | } 163 | 164 | .variables { 165 | 166 | --a: a; 167 | --b: var(--a); 168 | 169 | } -------------------------------------------------------------------------------- /tests/test-page/styles/aboutNetError.css: -------------------------------------------------------------------------------- 1 | body {background: black;} -------------------------------------------------------------------------------- /tests/test-page/testfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftEdge/css-usage/e2caffc6295bdd3a2aba61c2a7771924649c66eb/tests/test-page/testfont.ttf -------------------------------------------------------------------------------- /tests/unit-tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha Tests 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | -------------------------------------------------------------------------------- /tests/unit-tests/test.js: -------------------------------------------------------------------------------- 1 | // Basic Values 2 | describe('Basic Values', function(){ 3 | it('background-color', function(){ 4 | chai.assert.equal(["green"][0], CSSUsage.CSSValues.createValueArray("green")[0]); 5 | }); 6 | it('1-> ', function() { 7 | chai.assert.equal('', CSSUsage.CSSValues.parseValues("1")); 8 | }); 9 | it('1.5 -> ', function() { 10 | chai.assert.equal('', CSSUsage.CSSValues.parseValues("1.5")); 11 | }); 12 | it('1.5rem -> rem', function() { 13 | chai.assert.equal('rem', CSSUsage.CSSValues.parseValues("1.5rem")); 14 | }); 15 | it('.5em -> em', function() { 16 | chai.assert.equal('em', CSSUsage.CSSValues.parseValues(".5em")); 17 | }); 18 | it('2.5px -> px', function() { 19 | chai.assert.equal('px', CSSUsage.CSSValues.parseValues("2.5px")); 20 | }); 21 | it('6.785% -> %', function() { 22 | chai.assert.equal('%', CSSUsage.CSSValues.parseValues("6.785%")); 23 | }); 24 | it('-5.5em -> em', function() { 25 | chai.assert.equal('em', CSSUsage.CSSValues.parseValues("-5.5em")); 26 | }); 27 | it('-65px -> px', function() { 28 | chai.assert.equal('px', CSSUsage.CSSValues.parseValues("-65px")); 29 | }); 30 | it('#aaa -> #xxyyzz', function() { 31 | chai.assert.equal('#xxyyzz', CSSUsage.CSSValues.parseValues("#aaa")); 32 | }); 33 | it('#aaaaaa -> #xxyyzz', function() { 34 | chai.assert.equal('#xxyyzz', CSSUsage.CSSValues.parseValues("#aaaaaa")); 35 | }); 36 | it('table-cell', function() { 37 | chai.assert.equal('table-cell', CSSUsage.CSSValues.parseValues("table-cell")); 38 | }); 39 | }); 40 | 41 | describe('Basic Values UnNormalized', function(){ 42 | it('cursor: url("urlsomevalue.cur")', function(){ 43 | chai.assert.equal(["url(somevalue.cur)"][0], CSSUsage.CSSValues.createValueArray("url('somevalue.cur'), pointer", "cursor", true)[0]); 44 | }); 45 | }); 46 | 47 | // Webkit Values 48 | describe('Prefixed Values', function(){ 49 | it('-webkit-linear-gradient()', function(){ 50 | chai.assert.equal(["-webkit-linear-gradient()"][0], CSSUsage.CSSValues.createValueArray("-webkit-linear-gradient(red, blue)")[0]); 51 | }); 52 | }); 53 | 54 | // Shorthands 55 | describe('Shorthand & Complex Values', function() { 56 | it('background', function() { 57 | chai.assert.equal('linear-gradient()', CSSUsage.CSSValues.createValueArray('linear-gradient( 45deg, blue, red ) no-repeat')[0]); 58 | chai.assert.equal('no-repeat', CSSUsage.CSSValues.createValueArray('linear-gradient( 45deg, blue, red ) no-repeat')[1]); 59 | }); 60 | it('font-family: initial testing of comma seperated vals', function() { 61 | chai.assert.equal('baskerville,baskerville old face,hoefler text,garamond,times new roman,serif', CSSUsage.CSSValues.createValueArray('Baskerville,Baskerville Old Face,Hoefler Text,Garamond,Times New Roman,serif','font-family').join(',')); 62 | }); 63 | it('font-family: just more parsing due to bugs in initial implementation because this value sneaked through due to issue with indexOf', function() { 64 | chai.assert.equal('roboto', CSSUsage.CSSValues.createValueArray('roboto,arial,sans-serif','font-family')[0]); 65 | }); 66 | it('box-shadow: initial testing of comma seperated vals', function() { 67 | chai.assert.equal('3px,blue,1em,red', CSSUsage.CSSValues.createValueArray('3px blue , 1em red','box-shadow').join(',')); 68 | }); 69 | it('background: lime url(")...\\\\\\"\\\\\\...(") repeat', function() { 70 | chai.assert.equal('lime,url(),repeat', CSSUsage.CSSValues.createValueArray('lime url(")...\\\\\\"\\\\\\...(") repeat','background').join(',')); 71 | }); 72 | it('background-image: -webkit-gradient(linear, left bottom, left top, from(#5AE), to(#036))', function() { 73 | chai.assert.equal('-webkit-gradient()', CSSUsage.CSSValues.createValueArray('-webkit-gradient(linear, left bottom, left top, from(#5AE), to(#036))','background-image').join(',')); 74 | }); 75 | it('margin-left: calc(-1 * (100% - 15px))', function() { 76 | chai.assert.equal('calc()', CSSUsage.CSSValues.createValueArray('calc(-1 * (100% - 15px))','margin-left').join(',')); 77 | }); 78 | }); 79 | 80 | // Function Notation 81 | describe('Function Values', function() { 82 | it('rotate()', function() { 83 | chai.assert.equal('rotate()', CSSUsage.CSSValues.createValueArray("rotate(90deg)")[0]); 84 | }); 85 | it('hsla()', function() { 86 | chai.assert.equal('hsla()', CSSUsage.CSSValues.createValueArray("hsla(120, 100%, 50%, .75)")[0]); 87 | }); 88 | it('hsl()', function() { 89 | chai.assert.equal('hsl()', CSSUsage.CSSValues.createValueArray("hsl(120, 100%, 50%)")[0]); 90 | }); 91 | it('rgba()', function() { 92 | chai.assert.equal('rgba()', CSSUsage.CSSValues.createValueArray("rgba(200, 54, 54, 0.5)")[0]); 93 | }); 94 | it('matrix3d()', function() { 95 | chai.assert.equal('matrix3d()', CSSUsage.CSSValues.createValueArray("matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1)")[0]); 96 | }); 97 | it('matrix()', function() { 98 | chai.assert.equal('matrix()', CSSUsage.CSSValues.createValueArray("matrix(1, 0, 0, 1, 0, 0)")[0]); 99 | }); 100 | it('var()', function() { 101 | chai.assert.equal('var()', CSSUsage.CSSValues.createValueArray("var(--primary-color)")); 102 | }) 103 | }); 104 | 105 | describe('Matchable Selectors', function(){ 106 | it('a:hover -> a', function() { 107 | chai.assert.equal('a', CSSUsage.PropertyValuesAnalyzer.cleanSelectorText('a:hover')); 108 | }); 109 | it('*:hover -> *', function() { 110 | chai.assert.equal('*', CSSUsage.PropertyValuesAnalyzer.cleanSelectorText('*:hover')); 111 | }); 112 | it(':hover -> *', function() { 113 | chai.assert.equal('*', CSSUsage.PropertyValuesAnalyzer.cleanSelectorText(':hover')); 114 | }); 115 | it('#id .class:focus -> #id .class', function() { 116 | chai.assert.equal('#id .class', CSSUsage.PropertyValuesAnalyzer.cleanSelectorText('#id .class:focus')); 117 | }); 118 | it('ul li:not(:nth-child(2n+1)):active -> ul li:not(:nth-child(2n+1))', function() { 119 | chai.assert.equal('ul li:not(:nth-child(2n+1))', CSSUsage.PropertyValuesAnalyzer.cleanSelectorText('ul li:not(:nth-child(2n+1)):active')); 120 | }); 121 | it('a:before -> a', function() { 122 | chai.assert.equal('a', CSSUsage.PropertyValuesAnalyzer.cleanSelectorText('a:before')); 123 | }); 124 | it('a::before -> a', function() { 125 | chai.assert.equal('a', CSSUsage.PropertyValuesAnalyzer.cleanSelectorText('a::before')); 126 | }); 127 | }); 128 | 129 | describe('Generalized Selectors', function(){ 130 | it('#id1>#id2+#id3 -> #i > #i + #i', function() { 131 | chai.assert.equal('#i > #i + #i', CSSUsage.PropertyValuesAnalyzer.generalizedSelectorsOf('#id1>#id2+#id3')[0]); 132 | }); 133 | it('div.box:hover -> div.c:hover', function() { 134 | chai.assert.equal('div.c:hover', CSSUsage.PropertyValuesAnalyzer.generalizedSelectorsOf('div.box:hover')[0]); 135 | }); 136 | it('.class1.class2.class3 -> .c.c.c', function() { 137 | chai.assert.equal('.c.c.c', CSSUsage.PropertyValuesAnalyzer.generalizedSelectorsOf('.class1.class2.class3')[0]); 138 | }); 139 | it('.class1 .class2 .class3 -> .c .c .c', function() { 140 | chai.assert.equal('.c .c .c', CSSUsage.PropertyValuesAnalyzer.generalizedSelectorsOf('.class1 .class2 .class3')[0]); 141 | }); 142 | it('.-class1 ._class2 .Class3 -> .c .c .c', function() { 143 | chai.assert.equal('.c .c .c', CSSUsage.PropertyValuesAnalyzer.generalizedSelectorsOf('.-class1 ._class2 .Class3')[0]); 144 | }); 145 | it(':nth-child(2n+1) -> :nth-child', function() { 146 | chai.assert.equal(':nth-child', CSSUsage.PropertyValuesAnalyzer.generalizedSelectorsOf(':nth-child(2n+1)')[0]); 147 | }); 148 | it(':not([attribute*="-3-"]) -> :not', function() { 149 | chai.assert.equal(':not', CSSUsage.PropertyValuesAnalyzer.generalizedSelectorsOf(':not([attribute*="-3-"])')[0]); 150 | }); 151 | it('div.a#b[c][d].e#f -> div#i#i.c.c[a][a]', function() { 152 | chai.assert.equal('div#i#i.c.c[a][a]', CSSUsage.PropertyValuesAnalyzer.generalizedSelectorsOf('div.a#b[c][d].e#f')[0]); 153 | }); 154 | }); 155 | 156 | // Misc 157 | describe('Normalizing Values', function() { 158 | it('[font-family] remove fancy apostrophes', function() { 159 | chai.assert.equal('word', CSSUsage.CSSValues.parseValues('‘word’','font-family')); 160 | }); 161 | it('[font-family] remove regular apostrophes', function() { 162 | chai.assert.equal('word', CSSUsage.CSSValues.parseValues("'word'",'font-family')); 163 | }); 164 | it('trim values', function() { 165 | chai.assert.equal('word', CSSUsage.CSSValues.parseValues(" word ")); 166 | }); 167 | it('grid-template: bracket identifiers', function() { 168 | chai.assert.equal('', CSSUsage.CSSValues.parseValues('[start]','grid-template')); 169 | }); 170 | it('--var: bracket islands', function() { 171 | chai.assert.equal('', CSSUsage.CSSValues.parseValues('{toto:true}','--var')); 172 | }); 173 | }); --------------------------------------------------------------------------------