css-usage/Recipe.min.js

2068 строки
78 KiB
JavaScript
Исходник Постоянная ссылка Ответственный История

Этот файл содержит неоднозначные символы Юникода!

Этот файл содержит неоднозначные символы Юникода, которые могут быть перепутаны с другими в текущей локали. Если это намеренно, можете спокойно проигнорировать это предупреждение. Используйте кнопку Экранировать, чтобы подсветить эти символы.

void function() {
var _ = (a => new ArrayWrapper(a));
_.mapInline = mapInline;
_.map = map; /*.......*/ map.bind = (()=>map);
_.filter = filter; /*.*/ filter.bind = (()=>filter);
_.reduce = reduce; /*.*/ reduce.bind = (()=>reduce);
window.CSSUsageLodash = _;
// test case:
// 35 = CSSUsageLodash([1,2,3,4,5]).map(v => v*v).filter(v => v%2).reduce(0, (a,b)=>(a+b)).value()
function ArrayWrapper(array) {
this.source = array;
this.mapInline = function(f) { mapInline(this.source, f); return this; };
this.map = function(f) { this.source = map(this.source, f); return this; };
this.filter = function(f) { this.source = filter(this.source, f); return this; };
this.reduce = function(v,f) { this.source = reduce(this.source, f, v); return this; };
this.value = function() { return this.source };
}
function map(source, transform) {
var clone = new Array(source.length);
for(var i = source.length; i--;) {
clone[i] = transform(source[i]);
}
return clone;
}
function mapInline(source, transform) {
for(var i = source.length; i--;) {
source[i] = transform(source[i]);
}
return source;
}
function filter(source, shouldValueBeIncluded) {
var clone = new Array(source.length), i=0;
for(var s = 0; s <= source.length; s++) {
var value = source[s];
if(shouldValueBeIncluded(value)) {
clone[i++] = value
}
}
clone.length = i;
return clone;
}
function reduce(source, computeReduction, reduction) {
for(var s = 0; s <= source.length; s++) {
var value = source[s];
reduction = computeReduction(reduction, value);
}
return reduction;
}
}();
/*!
* Based on:
* https://github.com/gilmoreorless/css-shorthand-properties
* MIT Licensed: http://gilmoreorless.mit-license.org/
*/
void function () {
/**
* Data collated from multiple W3C specs: http://www.w3.org/Style/CSS/current-work
*/
var shorthands = this.shorthandProperties = {
// CSS 2.1: http://www.w3.org/TR/CSS2/propidx.html
'list-style': ['-type', '-position', '-image'],
'margin': ['-top', '-right', '-bottom', '-left'],
'outline': ['-width', '-style', '-color'],
'padding': ['-top', '-right', '-bottom', '-left'],
// CSS Backgrounds and Borders Module Level 3: http://www.w3.org/TR/css3-background/
'background': ['-image', '-position', '-size', '-repeat', '-origin', '-clip', '-attachment', '-color'],
'background-repeat': ['-x','-y'],
'background-position': ['-x','-y'],
'border': ['-width', '-style', '-color'],
'border-color': ['border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color'],
'border-style': ['border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style'],
'border-width': ['border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width'],
'border-top': ['-width', '-style', '-color'],
'border-right': ['-width', '-style', '-color'],
'border-bottom': ['-width', '-style', '-color'],
'border-left': ['-width', '-style', '-color'],
'border-radius': ['border-top-left-radius', 'border-top-right-radius', 'border-bottom-right-radius', 'border-bottom-left-radius'],
'border-image': ['-source', '-slice', '-width', '-outset', '-repeat'],
// CSS Fonts Module Level 3: http://www.w3.org/TR/css3-fonts/
'font': ['-style', '-variant', '-weight', '-stretch', '-size', 'line-height', '-family'],
'font-variant': ['-ligatures', '-alternates', '-caps', '-numeric', '-east-asian'],
// CSS Masking Module Level 1: http://www.w3.org/TR/css-masking/
'mask': ['-image', '-mode', '-position', '-size', '-repeat', '-origin', '-clip'],
'mask-border': ['-source', '-slice', '-width', '-outset', '-repeat', '-mode'],
// CSS Multi-column Layout Module: http://www.w3.org/TR/css3-multicol/
'columns': ['column-width', 'column-count'],
'column-rule': ['-width', '-style', '-color'],
// CSS Speech Module: http://www.w3.org/TR/css3-speech/
'cue': ['-before', '-after'],
'pause': ['-before', '-after'],
'rest': ['-before', '-after'],
// CSS Text Decoration Module Level 3: http://www.w3.org/TR/css-text-decor-3/
'text-decoration': ['-line', '-style', '-color'],
'text-emphasis': ['-style', '-color'],
// CSS Animations (WD): http://www.w3.org/TR/css3-animations
'animation': ['-name', '-duration', '-timing-function', '-delay', '-iteration-count', '-direction', '-fill-mode', '-play-state'],
// CSS Transitions (WD): http://www.w3.org/TR/css3-transitions/
'transition': ['-property', '-duration', '-timing-function', '-delay'],
// CSS Flexible Box Layout Module Level 1 (WD): http://www.w3.org/TR/css3-flexbox/
'flex': ['-grow', '-shrink', '-basis'],
// CSS Grid: https://drafts.csswg.org/css-grid/#grid-shorthand
'grid': ['-template', '-auto-flow', '-auto-rows','-auto-columns'],
'grid-template': ['-rows', '-columns', '-areas'],
// Others:
'overflow': ['-x','-y','-style'], // https://drafts.csswg.org/css-overflow-3/
};
var expandCache = Object.create(null);
var unexpandCache = Object.create(null);
/**
* Expand a shorthand property into an array of longhand properties which are set by it
* @param {string} property CSS property name
* @return {array} List of longhand properties, or an empty array if it's not a shorthand
*/
this.expand = function (property) {
var result = expandCache[property];
if(result) { return result; }
var prefixData = property.match(/^(-[a-zA-Z]+-)?(.*)$/);
var prefix = prefixData[1]||'', prefixFreeProperty = prefixData[2]||'';
if (!shorthands.hasOwnProperty(prefixFreeProperty)) {
return [];
}
result = [];
shorthands[prefixFreeProperty].forEach((p) => {
var longhand = p[0] === '-' ? property + p : prefix + p;
result.push(longhand);
result.push.apply(result, this.expand(longhand));
});
return expandCache[property] = result;
};
/**
* Expand a longhand property into an array of shorthand which may set the value
* @param {string} property CSS property name
* @return {array} List of shorthand properties, or the original property if it's not a shorthand
*/
this.unexpand = function unexpand(property) {
var result = unexpandCache[property];
if(result) { return result; }
var prefixData = property.match(/^(-[a-zA-Z]+-)?(.*)$/);
var prefix = prefixData[1]||'', prefixFreeProperty = prefixData[2]||'';
result = [];
for(var sh = 0; sh <= shorthands.length; sh++) {
var shorthand = shorthands[sh];
if(this.expand(shorthand).indexOf(prefixFreeProperty) >= 0) {
result.push(prefix+shorthand);
result.push.apply(result,this.unexpand(prefix+shorthand));
}
}
return unexpandCache[property] = result;
}
}.call(window.CSSShorthands={});
// http://blueprintcss.org/tests/parts/grid.html
var hasBluePrintUsage = function() {
if(!document.querySelector(".container")) {
return false;
}
for(var i = 24+1; --i;) {
if(document.querySelector(".container > .span-"+i)) {
return true;
}
}
return false;
}
//
// report how many times the classes in the following arrays have been used in the dom
// (bootstrap stats)
//
var detectedBootstrapGridUsages = function(domClasses) {
var _ = window.CSSUsageLodash;
var reduce = _.reduce.bind(_);
var trackedClasses = [];
var sizes = ['xs','sm','md','lg'];
for(var i = sizes.length; i--;) { var size = sizes[i];
for(var j = 12+1; --j;) {
trackedClasses.push('col-'+size+'-'+j);
for(var k = 12+1; --k;) {
trackedClasses.push('col-'+size+'-'+j+'-offset-'+k);
trackedClasses.push('col-'+size+'-'+j+'-push-'+k);
trackedClasses.push('col-'+size+'-'+j+'-pull-'+k);
}
}
}
return reduce(trackedClasses, (a,b) => a+(domClasses[b]|0), 0);
};
var detectedBootstrapFormUsages = function(domClasses) {
var _ = window.CSSUsageLodash;
var reduce = _.reduce.bind(_);
var trackedClasses = [
'form-group', 'form-group-xs', 'form-group-sm', 'form-group-md', 'form-group-lg',
'form-control', 'form-horizontal', 'form-inline',
'btn','btn-primary','btn-secondary','btn-success','btn-warning','btn-danger','btn-error'
];
return reduce(trackedClasses, (a,b) => a+(domClasses[b]|0), 0);
};
var detectedBootstrapAlertUsages = function(domClasses) {
var _ = window.CSSUsageLodash;
var reduce = _.reduce.bind(_);
var trackedClasses = [
'alert','alert-primary','alert-secondary','alert-success','alert-warning','alert-danger','alert-error'
];
return reduce(trackedClasses, (a,b) => a+(domClasses[b]|0), 0);
};
var detectedBootstrapFloatUsages = function(domClasses) {
var _ = window.CSSUsageLodash;
var reduce = _.reduce.bind(_);
var trackedClasses = [
'pull-left','pull-right',
];
return reduce(trackedClasses, (a,b) => a+(domClasses[b]|0), 0);
};
// https://github.com/Dogfalo/materialize/blob/master/sass/components/_grid.scss
var hasDogfaloMaterializeUsage = function() {
if(!document.querySelector(".container > .row > .col")) {
return false;
}
for(var i = 12+1; --i;) {
var classesToLookUp = ['s','m','l'];
for(var d = 0; d < classesToLookUp.length; d++) {
var s = classesToLookUp[d];
if(document.querySelector(".container > .row > .col."+s+""+i)) {
return true;
}
}
}
return false;
}
// http://www.gumbyframework.com/docs/grid/#!/basic-grid
var hasGrumbyUsage = function() {
if(!document.querySelector(".row .columns")) {
return false;
}
var classesToLookUp = ["one","two","three","four","five","six","seven","eight","nine","ten","eleven","twelve"];
for(var cl = 0; cl < classesToLookUp.length; cl++ ) {
var fraction = classesToLookUp[cl];
if(document.querySelector(".row > .columns."+fraction)) {
return true;
}
}
return false;
}
// https://raw.githubusercontent.com/csswizardry/inuit.css/master/generic/_widths.scss
var hasInuitUsage = function() {
if(!document.querySelector(".grid .grid__item")) {
return false;
}
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"];
for(var cu = 0; cu < classesToLookUp.length; cu++ ) {
var fraction = classesToLookUp[cu];
var subClassesToLookUp = ["","palm-","lap-","portable-","desk-"];
for(var sc = 0; sc < subClassesToLookUp.length; sc++) {
var ns = subClassesToLookUp[sc];
if(document.querySelector(".grid > .grid__item."+ns+fraction)) {
return true;
}
}
}
return false;
}
var getLonelyGatesUsage = function (cssLonelyClassGates, domClasses, domIds, cssLonelyIdGates) {
var _ = window.CSSUsageLodash;
if((cssLonelyClassGates || domClasses || domIds || cssLonelyIdGates) == undefined) return;
// get arrays of the .class gates used ({"hover":5} => ["hover"]), filter irrelevant entries
var cssUniqueLonelyClassGatesArray = Object.keys(cssLonelyClassGates);
var cssUniqueLonelyClassGatesUsedArray = _(cssUniqueLonelyClassGatesArray).filter((c) => domClasses[c]).value();
var cssUniqueLonelyClassGatesUsedWorthArray = _(cssUniqueLonelyClassGatesUsedArray).filter((c)=>(cssLonelyClassGates[c]>9)).value();
if(window.debugCSSUsage) if(window.debugCSSUsage) console.log(cssLonelyClassGates);
if(window.debugCSSUsage) if(window.debugCSSUsage) console.log(cssUniqueLonelyClassGatesUsedWorthArray);
// get arrays of the #id gates used ({"hover":5} => ["hover"]), filter irrelevant entries
var cssUniqueLonelyIdGatesArray = Object.keys(cssLonelyIdGates);
var cssUniqueLonelyIdGatesUsedArray = _(cssUniqueLonelyIdGatesArray).filter((c) => domIds[c]).value();
var cssUniqueLonelyIdGatesUsedWorthArray = _(cssUniqueLonelyIdGatesUsedArray).filter((c)=>(cssLonelyIdGates[c]>9)).value();
if(window.debugCSSUsage) if(window.debugCSSUsage) console.log(cssLonelyIdGates);
if(window.debugCSSUsage) if(window.debugCSSUsage) console.log(cssUniqueLonelyIdGatesUsedWorthArray);
}
//
// report how many times the classes in the following arrays have been used as css gate
// (modernizer stats)
//
// https://modernizr.com/docs#features
var detectedModernizerUsages = function(cssLonelyClassGates) {
if((cssLonelyClassGates) == undefined) return;
var ModernizerUsages = {count:0,values:{/* "js":1, "no-js":2 */}};
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"];
for(var tc = 0; tc < trackedClasses.length; tc++) {
var c = trackedClasses[tc];
countInstancesOfTheClass(c);
countInstancesOfTheClass('no-'+c);
}
return ModernizerUsages;
function countInstancesOfTheClass(c) {
var count = cssLonelyClassGates[c]; if(!count) return;
ModernizerUsages.count += count;
ModernizerUsages.values[c]=count;
}
}
function getFwkUsage(results, cssLonelyClassGates, domClasses, domIds, cssLonelyIdGates, cssClasses) {
// Modernizer
getLonelyGatesUsage(cssLonelyClassGates, domClasses, domIds, cssLonelyIdGates);
detectedModernizerUsages(cssLonelyClassGates);
results.FwkModernizer = !!window.Modernizer;
results.FwkModernizerDOMUsages = detectedModernizerUsages(domClasses);
results.FwkModernizerCSSUsages = detectedModernizerUsages(cssLonelyClassGates);
// Bootstrap
results.FwkBootstrap = !!((window.jQuery||window.$) && (window.jQuery||window.$).fn && (window.jQuery||window.$).fn.modal)|0;
results.FwkBootstrapGridUsage = detectedBootstrapGridUsages(domClasses);
results.FwkBootstrapFormUsage = detectedBootstrapFormUsages(domClasses);
results.FwkBootstrapFloatUsage = detectedBootstrapFloatUsages(domClasses);
results.FwkBootstrapAlertUsage = detectedBootstrapAlertUsages(domClasses);
results.FwkBootstrapGridRecognized = detectedBootstrapGridUsages(cssClasses);
results.FwkBootstrapFormRecognized = detectedBootstrapFormUsages(cssClasses);
results.FwkBootstrapFloatRecognized = detectedBootstrapFloatUsages(cssClasses);
results.FwkBootstrapAlertRecognized = detectedBootstrapAlertUsages(cssClasses);
// Grumby
results.FwkGrumby = hasGrumbyUsage()|0;
// Inuit
results.FwkInuit = hasInuitUsage()|0;
// Blueprint
results.FwkBluePrint = hasBluePrintUsage()|0;
// Dog Falo
results.FwkDogfaloMaterialize = hasDogfaloMaterializeUsage()|0;
return results;
}
//
// report how many times the classes in the following arrays have been used in the dom
// (general stats)
//
/** count how many times the usual clearfix classes are used */
var detectedClearfixUsages = function(domClasses) {
var _ = window.CSSUsageLodash;
var reduce = _.reduce.bind(_);
var trackedClasses = [
'clearfix','clear',
];
return reduce(trackedClasses, (a,b) => a+(domClasses[b]|0), 0);
};
/** count how many times the usual hide/show classes are used */
var detectedVisibilityUsages = function(domClasses) {
var _ = window.CSSUsageLodash;
var reduce = _.reduce.bind(_);
var trackedClasses = [
'show', 'hide', 'visible', 'hidden',
];
return reduce(trackedClasses, (a,b) => a+(domClasses[b]|0), 0);
};
function getPatternUsage(results, domClasses, cssClasses) {
results.PatClearfixUsage = detectedClearfixUsages(domClasses);
results.PatVisibilityUsage = detectedVisibilityUsages(domClasses);
results.PatClearfixRecognized = detectedClearfixUsages(cssClasses);
results.PatVisibilityRecognized = detectedVisibilityUsages(cssClasses);
return results;
}
void function() {
window.HtmlUsage = {};
// This function has been added to the elementAnalyzers in
// CSSUsage.js under onready()
// <param name="element"> is an HTMLElement passed in by elementAnalyzers
window.HtmlUsage.GetNodeName = function (element) {
// If the browser doesn't recognize the element - throw it away
if(element instanceof HTMLUnknownElement) {
return;
}
var node = element.nodeName;
var tags = HtmlUsageResults.tags || (HtmlUsageResults.tags = {});
var tag = tags[node] || (tags[node] = 0);
tags[node]++;
GetAttributes(element, node);
}
function GetAttributes(element, node) {
for(var i = 0; i < element.attributes.length; i++) {
var att = element.attributes[i];
if(IsValidAttribute(element, att.nodeName)) {
var attributes = HtmlUsageResults.attributes || (HtmlUsageResults.attributes = {});
var attribute = attributes[att.nodeName] || (attributes[att.nodeName] = {});
var attributeTag = attribute[node] || (attribute[node] = {count: 0});
attributeTag.count++;
}
}
}
function IsValidAttribute(element, attname) {
// We need to convert className
if(attname == "class") {
attname = "className";
}
if(attname == "classname") {
return false;
}
// Only keep attributes that are not data
if(attname.indexOf('data-') != -1) {
return false;
}
if(typeof(element[attname]) == "undefined") {
return false;
}
return true;
}
}();
void function() { try {
var _ = window.CSSUsageLodash;
var map = _.map.bind(_);
var mapInline = _.mapInline ? _.mapInline : map;
var reduce = _.reduce.bind(_);
var filter = _.filter.bind(_);
var browserIsEdge = navigator.userAgent.indexOf('Edge')>=0;
var browserIsFirefox = navigator.userAgent.indexOf('Firefox')>=0;
//
// Guards execution against invalid conditions
//
void function() {
// Don't run in subframes for now
if (top.location.href !== location.href) throw new Error("CSSUsage: the script doesn't run in frames for now");
// Don't run if already ran
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");
// Don't run if we don't have lodash
if (!window.CSSUsageLodash) throw new Error("CSSUsage: missing CSSUsageLodash dependency");
if (!window.HtmlUsage) throw new Error("APIUsage: missing HtmlUsage dependancy");
// Do not allow buggy trim() to bother usage
if((''+String.prototype.trim).indexOf("[native code]") == -1) {
console.warn('Replaced custom trim function with something known to work. Might break website.');
String.prototype.trim = function() {
return this.replace(/^\s+|\s+$/g, '');
}
}
}();
//
// Prepare our global namespace
//
void function() {
if(window.debugCSSUsage) console.log("STAGE: Building up namespace");
window.HtmlUsageResults = {
// this will contain all of the HTML tags used on a page
tags: {}, /*
tags ~= [nodeName] */
// this will contain all of the attributes used on an HTML tag
// and their values if they are in the whitelist
attributes: {} /*
attributes ~= {
name: <string>, // The name of the attribute
tag: <string>, // The tag that the attr was used on
value: <string> // The value of the attr
} */
};
window.RecipeResults = {};
window.Recipes = {
recipes: []
};
window.CSSUsage = {};
window.CSSUsageResults = {
// this will contain the usage stats of various at-rules and rules
types: [ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, ], /*
types ~= {
"unknown":0, //0
"style":0, //1
"charset": 0, //2
"import":0, //3
"media":0, //4
"font-face":0, //5
"page":0, //6
"keyframes":0, //7 This is the @keyframe at rule
"keyframe":0, //8 This is the individual 0%, or from/to
"reserved9":0, //9
"namespace":0, //10
"reserved11":0,//11
"supports":0, //12
"reserved13":0,//13
"reserved14":0,//14
"viewport":0, //15
}*/
// this will contain the usage stats of various css properties and values
props: Object.create(null), /*
props ~= {
"background-color": {
count: 10,
values: {
"<color-keyword>": 9,
"inherit": 1
}
}
}*/
// this will contains the various datapoints we measure on css selector usage
usages: {"SuccessfulCrawls":1},
// this will contain selectors and the properties they refer to
rules: {"@stylerule":0,"@atrule":0,"@inline":0}, /*
rules ~= {
"#id:hover .class": {
count: 10,
props: {
"background-color": 5,
"color": 4,
"opacity": 3,
"transform": 3
}
}
}*/
atrules: {}/*
atrules ~= {
"@atrule:4": {
count: 3,
props: {
"background-color": 1,
"color": 4,
"opacity": 3,
"transform": 3
},
nested: {
"h3": 1
},
conditions: {
"screen": 1
}
}
}*/
}
}();
//
// The StyleWalker API cover the extraction of style in the browser
//
void function() { "use strict";
CSSUsage.StyleWalker = {
// This array contains the list of functions being run on each CSSStyleDeclaration
// [ function(style, selectorText, matchedElements, ruleType) { ... }, ... ]
ruleAnalyzers: [],
// This array contains the list of functions being run on each DOM element of the page
// [ function(element) { ...} ]
elementAnalyzers: [],
recipesToRun: [],
runRecipes: false,
//
walkOverCssStyles: walkOverCssStyles,
walkOverDomElements: walkOverDomElements,
// Those stats are being collected while walking over the css style rules
amountOfInlineStyles: 0,
amountOfSelectorsUnused: 0,
amountOfSelectors: 0,
}
var hasWalkedDomElementsOnce = false;
// holds @keyframes temporarily while we wait to know how much they are used
var keyframes = Object.create(null);
/**
* For all stylesheets of the document,
* walk through the stylerules and run analyzers
*/
function walkOverCssStyles() {
if(window.debugCSSUsage) console.log("STAGE: Walking over styles");
var styleSheets = document.styleSheets;
// Loop through StyeSheets
for (var ssIndex = styleSheets.length; ssIndex--;) {
var styleSheet = styleSheets[ssIndex];
try {
if(styleSheet.cssRules) {
walkOverCssRules(styleSheet.cssRules, styleSheet);
} else {
console.warn("No content loaded for stylesheet: ", styleSheet.href||styleSheet);
}
}
catch (e) {
if(window.debugCSSUsage) console.log(e, e.stack);
}
}
// Hack: rely on the results to find out which
// animations actually run, and parse their keyframes
var animations = (CSSUsageResults.props['animation-name']||{}).values||{};
for(var animation in keyframes) {
var keyframe = keyframes[animation];
var matchCount = animations[animation]|0;
var fakeElements = initArray(matchCount, (i)=>({tagName:'@keyframes '+animation+' ['+i+']'}));
processRule(keyframe, fakeElements);
}
}
/**
* This is the css work horse, this will will loop over the
* rules and then call the rule analyzers currently registered
*/
function walkOverCssRules(/*CSSRuleList*/ cssRules, styleSheet, parentMatchedElements) {
if(window.debugCSSUsage) console.log("STAGE: Walking over rules");
for (var ruleIndex = cssRules.length; ruleIndex--;) {
// Loop through the rules
var rule = cssRules[ruleIndex];
// Until we can correlate animation usage
// to keyframes do not parse @keyframe rules
if(rule.type == 7) {
keyframes[rule.name] = rule;
continue;
}
// Filter "@supports" which the current browser doesn't support
if(rule.type == 12 && (!CSS.supports || !CSS.supports(rule.conditionText))) {
continue;
}
// Other rules should be processed immediately
processRule(rule,parentMatchedElements);
}
}
/**
* This function takes a css rule and:
* [1] walk over its child rules if needed
* [2] call rule analyzers for that rule if it has style data
*/
function processRule(rule, parentMatchedElements) {
// Increment the rule type's counter
CSSUsageResults.types[rule.type|0]++;
// Some CssRules have nested rules to walk through:
if (rule.cssRules && rule.cssRules.length>0) {
walkOverCssRules(rule.cssRules, rule.parentStyleSheet, parentMatchedElements);
}
// Some CssRules have style we can analyze
if(rule.style) {
// find what the rule applies to
var selectorText;
var matchedElements;
if(rule.selectorText) {
selectorText = CSSUsage.PropertyValuesAnalyzer.cleanSelectorText(rule.selectorText);
try {
if(parentMatchedElements) {
matchedElements = [].slice.call(document.querySelectorAll(selectorText));
matchedElements.parentMatchedElements = parentMatchedElements;
} else {
matchedElements = [].slice.call(document.querySelectorAll(selectorText));
}
} catch(ex) {
matchedElements = [];
console.warn(ex.stack||("Invalid selector: "+selectorText+" -- via "+rule.selectorText));
}
} else {
selectorText = '@atrule:'+rule.type;
if(parentMatchedElements) {
matchedElements = parentMatchedElements;
} else {
matchedElements = [];
}
}
// run an analysis on it
runRuleAnalyzers(rule.style, selectorText, matchedElements, rule.type);
}
// run analysis on at rules to populate CSSUsageResults.atrules
if(isRuleAnAtRule(rule)) {
if(rule.conditionText) {
processConditionalAtRules(rule);
} else {
processGeneralAtRules(rule);
}
}
}
/**
* Checks whether an rule is an @atrule.
*/
function isRuleAnAtRule(rule) {
/**
* @atrules types ~= {
"charset": 0, //2
"import":0, //3
"media":0, //4
"font-face":0, //5
"page":0, //6
"keyframes":0, //7 This is the @keyframe at rule
"keyframe":0, //8 This is the individual 0%, or from/to
"namespace":0, //10
"supports":0, //12
"viewport":0, //15
*/
let type = rule.type;
return (type >= 2 && type <= 8) || (type == 10) || (type == 12) || (type == 15);
}
/**
* This process @atrules with conditional statements such as @supports.
* [1] It will process any props and values used within the body of the rule.
* [2] It will count the occurence of usage of nested atrules.
* [3] It will process condition statements to conform to a standardized version.
*/
function processConditionalAtRules(rule) {
var selectorText = '@atrule:' + rule.type;
var atrulesUsage = CSSUsageResults.atrules;
if(!atrulesUsage[selectorText]) {
atrulesUsage[selectorText] = Object.create(null);
atrulesUsage[selectorText] = {"count": 1,
"props": {},
"conditions": {}}
} else {
var count = atrulesUsage[selectorText].count;
atrulesUsage[selectorText].count = count + 1;
}
var selectedAtruleUsage = atrulesUsage[selectorText];
if(rule.cssRules) {
CSSUsage.PropertyValuesAnalyzer.anaylzeStyleOfRulePropCount(rule, selectedAtruleUsage);
}
processConditionText(rule.conditionText, selectedAtruleUsage.conditions);
}
/**
* This processes the usage of conditions of conditional @atrules like @media.
* Requires the condition of the rule to process and the current recorded usage
* of the @atrule in question.
*/
function processConditionText(conditionText, selectedAtruleConditionalUsage) {
// replace numeric specific information from condition statements
conditionText = CSSUsage.CSSValues.parseValues(conditionText);
if(!selectedAtruleConditionalUsage[conditionText]) {
selectedAtruleConditionalUsage[conditionText] = Object.create(null);
selectedAtruleConditionalUsage[conditionText] = {"count": 1}
} else {
var count = selectedAtruleConditionalUsage[conditionText].count;
selectedAtruleConditionalUsage[conditionText].count = count + 1;
}
}
/**
* This will process all other @atrules that don't have conditions or styles.
* [1] It will process any props and values used within the body of the rule.
* [2] It will count the occurence of usage of nested atrules.
*/
function processGeneralAtRules(rule) {
var selectorText = '@atrule:' + rule.type;
var atrulesUsage = CSSUsageResults.atrules;
if(!atrulesUsage[selectorText]) {
atrulesUsage[selectorText] = Object.create(null);
atrulesUsage[selectorText] = {"count": 1,
"props": {}}
} else {
var count = atrulesUsage[selectorText].count;
atrulesUsage[selectorText].count = count + 1;
}
// @keyframes rule type is 7
if(rule.type == 7) {
processKeyframeAtRules(rule);
} else if(CSSUsageResults.rules[selectorText].props) {
atrulesUsage[selectorText].props = CSSUsageResults.rules[selectorText].props;
delete atrulesUsage[selectorText].props.values;
}
}
/**
* Processes on @keyframe to add the appropriate props from the frame and a counter of which
* frames are used throughout the document.
*/
function processKeyframeAtRules(rule) {
var selectorText = '@atrule:' + rule.type;
var atrulesUsageForSelector = CSSUsageResults.atrules[selectorText];
if(!atrulesUsageForSelector["keyframes"]) {
atrulesUsageForSelector["keyframes"] = Object.create(null);
}
/**
* grab the props from the individual keyframe props that was already populated
* under CSSUsageResults.rules. Note: @atrule:8 is the individual frames.
* WARN: tightly coupled with previous processing of rules.
*/
atrulesUsageForSelector.props = CSSUsageResults.rules["@atrule:8"].props;
delete atrulesUsageForSelector.props.values;
for(let index in rule.cssRules) {
let keyframe = rule.cssRules[index];
var atrulesUsageForKeyframeOfSelector = atrulesUsageForSelector.keyframes;
if(!keyframe.keyText) {
continue;
}
var frame = keyframe.keyText;
// replace extra whitespaces
frame = frame.replace(/\s/g, '');
if(!atrulesUsageForKeyframeOfSelector[frame]) {
atrulesUsageForKeyframeOfSelector[frame] = { "count" : 1 };
} else {
var keyframeCount = atrulesUsageForKeyframeOfSelector[frame].count;
atrulesUsageForKeyframeOfSelector[frame].count = keyframeCount + 1;
}
}
}
/**
* This is the dom work horse, this will will loop over the
* dom elements and then call the element analyzers currently registered,
* as well as rule analyzers for inline styles
*/
function walkOverDomElements(obj, index) {
if(window.debugCSSUsage) console.log("STAGE: Walking over DOM elements");
var recipesToRun = CSSUsage.StyleWalker.recipesToRun;
obj = obj || document.documentElement; index = index|0;
// Loop through the elements
var elements = [].slice.call(document.all,0);
for(var i = 0; i < elements.length; i++) {
var element=elements[i];
// Analyze its style, if any
if(!CSSUsage.StyleWalker.runRecipes) {
// Analyze the element
runElementAnalyzers(element, index);
if (element.hasAttribute('style')) {
// Inline styles count like a style rule with no selector but one matched element
var ruleType = 1;
var isInline = true;
var selectorText = '@inline:'+element.tagName;
var matchedElements = [element];
runRuleAnalyzers(element.style, selectorText, matchedElements, ruleType, isInline);
}
} else { // We've already walked the DOM crawler and need to run the recipes
for(var r = 0; r < recipesToRun.length ; r++) {
var recipeToRun = recipesToRun[r];
var results = RecipeResults[recipeToRun.name] || (RecipeResults[recipeToRun.name]={});
recipeToRun(element, results, true);
}
}
}
}
/**
* Given a rule and its data, send it to all rule analyzers
*/
function runRuleAnalyzers(style, selectorText, matchedElements, type, isInline) {
// Keep track of the counters
if(isInline) {
CSSUsage.StyleWalker.amountOfInlineStyles++;
} else {
CSSUsage.StyleWalker.amountOfSelectors++;
}
// Run all rule analyzers
for(var i = 0; i < CSSUsage.StyleWalker.ruleAnalyzers.length; i++) {
var runAnalyzer = CSSUsage.StyleWalker.ruleAnalyzers[i];
runAnalyzer(style, selectorText, matchedElements, type, isInline);
}
}
/**
* Given an element and its data, send it to all element analyzers
*/
function runElementAnalyzers(element, index, depth) {
for(var i = 0; i < CSSUsage.StyleWalker.elementAnalyzers.length; i++) {
var runAnalyzer = CSSUsage.StyleWalker.elementAnalyzers[i];
runAnalyzer(element, index, depth);
}
}
/**
* Creates an array of "length" elements, by calling initializer for each cell
*/
function initArray(length, initializer) {
var array = Array(length);
for(var i = length; i--;) {
array[i] = initializer(i);
}
return array;
}
}();
//
// helper to work with css values
//
void function() {
CSSUsage.CSSValues = {
createValueArray: createValueArray,
parseValues: parseValues,
normalizeValue: createValueArray
};
/**
* This will take a string value and reduce it down
* to only the aspects of the value we wish to keep
*/
function parseValues(value,propertyName) {
// Trim value on the edges
value = value.trim();
// Normalize letter-casing
value = value.toLowerCase();
// Map colors to a standard value (eg: white, blue, yellow)
if (isKeywordColor(value)) { return "<color-keyword>"; }
value = value.replace(/[#][0-9a-fA-F]+/g, '#xxyyzz');
// Escapce identifiers containing numbers
var numbers = ['ZERO','ONE','TWO','THREE','FOUR','FIVE','SIX','SEVEN','EIGHT','NINE'];
value = value.replace(
/([_a-z][-_a-z]|[_a-df-z])[0-9]+[-_a-z0-9]*/g,
s=>numbers.reduce(
(m,nstr,nint)=>m.replace(RegExp(nint,'g'),nstr),
s
)
);
// Remove any digits eg: 55px -> px, 1.5 -> 0.0, 1 -> 0
value = value.replace(/(?:[+]|[-]|)(?:(?:[0-9]+)(?:[.][0-9]+|)|(?:[.][0-9]+))(?:[e](?:[+]|[-]|)(?:[0-9]+))?(%|e[a-z]+|[a-df-z][a-z]*)/g, "$1");
value = value.replace(/(?:[+]|[-]|)(?:[0-9]+)(?:[.][0-9]+)(?:[e](?:[+]|[-]|)(?:[0-9]+))?/g, " <float> ");
value = value.replace(/(?:[+]|[-]|)(?:[.][0-9]+)(?:[e](?:[+]|[-]|)(?:[0-9]+))?/g, " <float> ");
value = value.replace(/(?:[+]|[-]|)(?:[0-9]+)(?:[e](?:[+]|[-]|)(?:[0-9]+))/g, " <float> ");
value = value.replace(/(?:[+]|[-]|)(?:[0-9]+)/g, " <int> ");
// Unescapce identifiers containing numbers
value = numbers.reduce(
(m,nstr,nint)=>m.replace(RegExp(nstr,'g'),nint),
value
)
// Remove quotes
value = value.replace(/('|||")/g, "");
//
switch(propertyName) {
case 'counter-increment':
case 'counter-reset':
// Anonymize the user identifier
value = value.replace(/[-_a-zA-Z0-9]+/g,' <custom-ident> ');
break;
case 'grid':
case 'grid-template':
case 'grid-template-rows':
case 'grid-template-columns':
case 'grid-template-areas':
// Anonymize line names
value = value.replace(/\[[-_a-zA-Z0-9 ]+\]/g,' <line-names> ');
break;
case '--var':
// Replace (...), {...} and [...]
value = value.replace(/[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]*)[)])*[)])*[)])*[)])*[)]/g, " <parentheses-block> ");
value = value.replace(/[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]*)[)])*[)])*[)])*[)])*[)]/g, " <parentheses-block> ");
value = value.replace(/\[(?:[^()]+|\[(?:[^()]+|\[(?:[^()]+|\[(?:[^()]+|\[(?:[^()]*)\])*\])*\])*\])*\]/g, " <curly-brackets-block> ");
value = value.replace(/\[(?:[^()]+|\[(?:[^()]+|\[(?:[^()]+|\[(?:[^()]+|\[(?:[^()]*)\])*\])*\])*\])*\]/g, " <curly-brackets-block> ");
value = value.replace(/\{(?:[^()]+|\{(?:[^()]+|\{(?:[^()]+|\{(?:[^()]+|\{(?:[^()]*)\})*\})*\})*\})*\}/g, " <square-brackets-block> ");
value = value.replace(/\{(?:[^()]+|\{(?:[^()]+|\{(?:[^()]+|\{(?:[^()]+|\{(?:[^()]*)\})*\})*\})*\})*\}/g, " <square-brackets-block> ");
break;
}
return value.trim();
}
//-----------------------------------------------------------------------------
/**
* This will transform a value into an array of value identifiers
*/
function createValueArray(value, propertyName, dontNormalize = false) {
// Trim value on the edges
value = value.trim();
// Normalize letter-casing
value = value.toLowerCase();
// Do the right thing in function of the property
if (!dontNormalize) {
// Remove comments and !important
value = value.replace(/([/][*](?:.|\r|\n)*[*][/]|[!]important.*)/g,'');
switch (propertyName) {
case 'font-family':
// Remove various quotes
if (value.indexOf("'") != -1 || value.indexOf("") != -1 || value.indexOf('"')) {
value = value.replace(/('|||")/g, "");
}
// Divide at commas to separate different font names
value = value.split(/\s*,\s*/g);
return value;
case '--var':
// Replace strings by dummies
value = value.replace(/"([^"\\]|\\[^"\\]|\\\\|\\")*"/g,' <string> ')
value = value.replace(/'([^'\\]|\\[^'\\]|\\\\|\\')*'/g,' <string> ');
// Replace url(...) functions by dummies
value = value.replace(/([a-z]?)[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]*)[)])*[)])*[)])*[)])*[)]/g, "$1()");
value = value.replace(/([a-z]?)[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]*)[)])*[)])*[)])*[)])*[)]/g, "$1()");
// Remove group contents (...), {...} and [...]
value = value.replace(/[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]*)[)])*[)])*[)])*[)])*[)]/g, " <parentheses-block> ");
value = value.replace(/[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]*)[)])*[)])*[)])*[)])*[)]/g, " <parentheses-block> ");
value = value.replace(/[{](?:[^{}]+|[{](?:[^{}]+|[{](?:[^{}]+|[{](?:[^{}]+|[{](?:[^{}]*)[}])*[}])*[}])*[}])*[}]/g, " <curly-brackets-block> ");
value = value.replace(/[{](?:[^{}]+|[{](?:[^{}]+|[{](?:[^{}]+|[{](?:[^{}]+|[{](?:[^{}]*)[}])*[}])*[}])*[}])*[}]/g, " <curly-brackets-block> ");
value = value.replace(/[\[](?:[^\[\]]+|[\[](?:[^\[\]]+|[\[](?:[^\[\]]+|[\[](?:[^\[\]]+|[\[](?:[^\[\]]*)[\]])*[\]])*[\]])*[\]])*[\]]/g, " <square-brackets-block> ");
value = value.replace(/[\[](?:[^\[\]]+|[\[](?:[^\[\]]+|[\[](?:[^\[\]]+|[\[](?:[^\[\]]+|[\[](?:[^\[\]]*)[\]])*[\]])*[\]])*[\]])*[\]]/g, " <square-brackets-block> ");
break;
default:
// Replace strings by dummies
value = value.replace(/"([^"\\]|\\[^"\\]|\\\\|\\")*"/g,' <string> ')
.replace(/'([^'\\]|\\[^'\\]|\\\\|\\')*'/g,' <string> ');
// Replace url(...) functions by dummies
if (value.indexOf("(") != -1) {
value = value.replace(/([a-z]?)[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]*)[)])*[)])*[)])*[)])*[)]/g, "$1() ");
value = value.replace(/([a-z]?)[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]*)[)])*[)])*[)])*[)])*[)]/g, "$1() ");
}
}
}
else {
switch (propertyName) {
// 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
// to prevent inconsistencies in results (in some running cases they are put in, others not) and split on the fallback values supplied.
case 'cursor':
// 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).
if (value.indexOf("'") != -1 || value.indexOf("") != -1 || value.indexOf('"')) {
value = value.replace(/('|||")/g, "");
}
// Divide at commas to separate cursor url value and supplied fallback values.
value = value.split(/\s*,\s*/g);
return value;
}
}
// Collapse whitespace
value = value.trim().replace(/\s+/g, " ");
// Divide at commas and spaces to separate different values
value = value.split(/\s*(?:,|[/])\s*|\s+/g);
return value;
}
/**
* So that we don't end up with a ton of color
* values, this will determine if the color is a
* keyword color value
*/
function isKeywordColor(candidateColor) {
// Keyword colors from the W3C specs
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)$/;
return isColorKeyword.test(candidateColor);
}
}();
//
// computes various css stats (PropertyValuesAnalyzer)
//
void function() {
CSSUsage.PropertyValuesAnalyzer = analyzeStyleOfRule;
CSSUsage.PropertyValuesAnalyzer.cleanSelectorText = cleanSelectorText;
CSSUsage.PropertyValuesAnalyzer.generalizedSelectorsOf = generalizedSelectorsOf;
CSSUsage.PropertyValuesAnalyzer.finalize = finalize;
CSSUsage.PropertyValuesAnalyzer.anaylzeStyleOfRulePropCount = anaylzeStyleOfRulePropCount;
// We put a computed style in cache for filtering purposes
var defaultStyle = getComputedStyle(document.createElement('div'));
// As well as some basic lies
var getBuggyValuesForThisBrowser = function() {
var buggyValues = getBuggyValuesForThisBrowser.cache;
if(buggyValues) { return buggyValues; }
else { buggyValues = Object.create(null); }
// Edge reports initial value instead of "initial", we have to be cautious
if(browserIsEdge) {
buggyValues['*'] = 1; // make 0 values automatic for longhand properties
//buggyValues['list-style-position:outside'] = 0;
buggyValues['list-style-image:none'] = 1;
//buggyValues['outline-color:invert'] = 0;
//buggyValues['outline-style:none'] = 0;
//buggyValues['outline-width:medium'] = 0;
//buggyValues['background-image:none'] = 0;
//buggyValues['background-attachment:scroll'] = 0;
//buggyValues['background-repeat:repeat'] = 0;
//buggyValues['background-repeat-x:repeat'] = 0;
//buggyValues['background-repeat-y:repeat'] = 0;
//buggyValues['background-position-x:0%'] = 0;
//buggyValues['background-position-y:0%'] = 0;
//buggyValues['background-size:auto'] = 0;
//buggyValues['background-origin:padding-box'] = 0;
//buggyValues['background-clip:border-box'] = 0;
//buggyValues['background-color:transparent'] = 0;
buggyValues['border-top-color:currentcolor'] = 1;
buggyValues['border-right-color:currentcolor'] = 1;
buggyValues['border-bottom-color:currentcolor'] = 1;
buggyValues['border-left-color:currentcolor'] = 1;
//buggyValues['border-top-style:solid'] = 0;
//buggyValues['border-right-style:solid'] = 0;
//buggyValues['border-bottom-style:solid'] = 0;
//buggyValues['border-left-style:solid'] = 0;
buggyValues['border-top-width:medium'] = 1;
buggyValues['border-right-width:medium'] = 1;
buggyValues['border-bottom-width:medium'] = 1;
buggyValues['border-left-width:medium'] = 1;
buggyValues['border-image-source:none'] = 1;
buggyValues['border-image-outset:0'] = 1;
buggyValues['border-image-width:1'] = 1;
buggyValues['border-image-repeat:repeat'] = 1;
buggyValues['border-image-repeat-x:repeat'] = 1;
buggyValues['border-image-repeat-y:repeat'] = 1;
buggyValues['line-height:normal'] = 1;
//buggyValues['font-size-adjust:none'] = 0;
buggyValues['font-stretch:normal'] = 1;
}
// Firefox reports initial values instead of "initial", we have to be cautious
if(browserIsFirefox) {
buggyValues['*'] = 1; // make 0 values automatic for longhand properties
}
// Attempt to force to optimize the object somehow
Object.create(buggyValues);
return getBuggyValuesForThisBrowser.cache = buggyValues;
};
var valueExistsInRootProperty = (cssText,key,rootKey,value) => {
value = value.trim().toLowerCase();
// detect suspicious values
var buggyValues = getBuggyValuesForThisBrowser();
// apply common sense to the given value, per browser
var buggyState = buggyValues[key+':'+value];
if(buggyState === 1) { return false; }
if(buggyState !== 0 && (!buggyValues['*'] || CSSShorthands.unexpand(key).length == 0)) { return true; }
// root properties are unlikely to lie
if(key==rootKey) return false;
// ask the browser is the best we can do right now
var values = value.split(/\s+|\s*,\s*/g);
var validValues = ' ';
var validValuesExtractor = new RegExp(' '+rootKey+'(?:[-][-_a-zA-Z0-9]+)?[:]([^;]*)','gi');
var match; while(match = validValuesExtractor.exec(cssText)) {
validValues += match[1] + ' ';
}
for(var i = 0; i < values.length; i++) {
var value = values[i];
if(validValues.indexOf(' '+value+' ')==-1) return false;
}
return true;
};
/** This will loop over the styles declarations */
function analyzeStyleOfRule(style, selectorText, matchedElements, type, isInline) { isInline=!!isInline;
// We want to filter rules that are not actually used
var count = matchedElements.length;
var selector = selectorText;
var selectorCat = {'1:true':'@inline','1:false':'@stylerule'}[''+type+':'+isInline]||'@atrule';
// Keep track of unused rules
var isRuleUnused = (count == 0);
if(isRuleUnused) {
CSSUsage.StyleWalker.amountOfSelectorsUnused++;
}
// We need a generalized selector to collect some stats
var generalizedSelectors = (
(selectorCat=='@stylerule')
? [selectorCat].concat(generalizedSelectorsOf(selector))
: [selectorCat, selector]
);
// Get the datastores of the generalized selectors
var generalizedSelectorsData = map(generalizedSelectors, (generalizedSelector) => (
CSSUsageResults.rules[generalizedSelector] || (CSSUsageResults.rules[generalizedSelector] = {count:0,props:Object.create(null)})
));
// Increment the occurence counter of found generalized selectors
for(var i = 0; i < generalizedSelectorsData.length; i++) {
var generalizedSelectorData = generalizedSelectorsData[i];
generalizedSelectorData.count++
}
// avoid most common browser lies
var cssText = ' ' + style.cssText.toLowerCase();
if(browserIsEdge) {
cssText = cssText.replace(/border: medium; border-image: none;/,'border: none;');
cssText = cssText.replace(/ border-image: none;/,' ');
}
// For each property declaration in this rule, we collect some stats
for (var i = style.length; i--;) {
var key = style[i], rootKeyIndex=key.indexOf('-'), rootKey = rootKeyIndex==-1 ? key : key.substr(0,rootKeyIndex);
var normalizedKey = rootKeyIndex==0&&key.indexOf('-',1)==1 ? '--var' : key;
var styleValue = style.getPropertyValue(key);
// Only keep styles that were declared by the author
// We need to make sure we're only checking string props
var isValueInvalid = typeof styleValue !== 'string' && styleValue != "" && styleValue != undefined;
if (isValueInvalid) {
continue;
}
var isPropertyUndefined = (cssText.indexOf(' '+key+':') == -1) && (styleValue=='initial' || !valueExistsInRootProperty(cssText, key, rootKey, styleValue));
if (isPropertyUndefined) {
continue;
}
// divide the value into simplified components
var specifiedValuesArray = CSSUsage.CSSValues.createValueArray(styleValue,normalizedKey);
var specifiedValuesUnnormalized = CSSUsage.CSSValues.createValueArray(styleValue,normalizedKey,true);
var values = new Array();
for (var j = 0; j < specifiedValuesArray.length; ++j) {
values.push(CSSUsage.CSSValues.parseValues(specifiedValuesArray[j],normalizedKey));
}
// log the property usage per selector
for(var gs = 0; gs < generalizedSelectorsData.length; gs++) {
var generalizedSelectorData = generalizedSelectorsData[gs];
// get the datastore for current property
var propStats = generalizedSelectorData.props[normalizedKey] || (generalizedSelectorData.props[normalizedKey] = {count:0,values:Object.create(null)});
// we saw the property one time
propStats.count++;
// we also saw a bunch of values
for(var v = 0; v < values.length; v++) {
var value = values[v];
// increment the counts for those by one, too
if(value.length>0) {
propStats.values[value] = (propStats.values[value]|0) + 1
}
}
}
// if we may increment some counts due to this declaration
if(count > 0) {
// instanciate or fetch the property metadata
var propObject = CSSUsageResults.props[normalizedKey];
if (!propObject) {
propObject = CSSUsageResults.props[normalizedKey] = {
count: 0,
values: Object.create(null)
};
}
// update the occurence counts of the property and value
for(var e = 0; e < matchedElements.length; e++) {
var element = matchedElements[e];
// check what the elements already contributed for this property
var cssUsageMeta = element.CSSUsage || (element.CSSUsage=Object.create(null));
var knownValues = cssUsageMeta[normalizedKey] || (cssUsageMeta[normalizedKey] = []);
// For recipes, at times we want to look at the specified values as well so hang
// these on the element so we don't have to recompute them
knownValues.valuesArray = knownValues.valuesArray || (knownValues.valuesArray = []);
for(var sv = 0; sv < specifiedValuesUnnormalized.length; sv++) {
var currentSV = specifiedValuesUnnormalized[sv];
if(knownValues.valuesArray.indexOf(currentSV) == -1) {
knownValues.valuesArray.push(currentSV)
}
}
// increment the amount of affected elements which we didn't count yet
if(knownValues.length == 0) { propObject.count += 1; }
// add newly found values too
for(var v = 0; v < values.length; v++) {
var value = values[v];
// Just want to keep the first of each distinct value for the CSS property.
if (knownValues.indexOf(value) == -1) {
propObject.values[value] = (propObject.values[value] | 0) + 1;
knownValues.push(value);
}
}
}
}
}
}
function anaylzeStyleOfRulePropCount(rule, selectedAtrulesUsage) {
for(let index in rule.cssRules) {
let ruleBody = rule.cssRules[index];
let style = ruleBody.style;
// guard for non css objects
if(!style) {
continue;
}
if(ruleBody.selector) {
try {
var selectorText = CssPropertyValuesAnalyzer.cleanSelectorText(ruleBody.selectorText);
var matchedElements = [].slice.call(document.querySelectorAll(selectorText));
if (matchedElements.length == 0) {
continue;
}
} catch (ex) {
console.warn(ex.stack||("Invalid selector: "+selectorText+" -- via "+ruleBody.selectorText));
continue;
}
}
let cssText = ' ' + style.cssText.toLowerCase();
for (var i = style.length; i--;) {
// processes out normalized prop name for style
var key = style[i], rootKeyIndex=key.indexOf('-'), rootKey = rootKeyIndex==-1 ? key : key.substr(0,rootKeyIndex);
var normalizedKey = rootKeyIndex==0&&key.indexOf('-',1)==1 ? '--var' : key;
var styleValue = style.getPropertyValue(key);
// Only keep styles that were declared by the author
// We need to make sure we're only checking string props
var isValueInvalid = typeof styleValue !== 'string' && styleValue != "" && styleValue != undefined;
if (isValueInvalid) {
continue;
}
var isPropertyUndefined = (cssText.indexOf(' '+key+':') == -1) && (styleValue=='initial' || !valueExistsInRootProperty(cssText, key, rootKey, styleValue));
if (isPropertyUndefined) {
continue;
}
var propsForSelectedAtrule = selectedAtrulesUsage.props;
if(!propsForSelectedAtrule[normalizedKey]) {
propsForSelectedAtrule[normalizedKey] = Object.create(null);
propsForSelectedAtrule[normalizedKey] = {"count": 1};
} else {
var propCount = propsForSelectedAtrule[normalizedKey].count;
propsForSelectedAtrule[normalizedKey].count = propCount + 1;
}
}
}
}
function finalize() {
// anonymize identifiers used for animation-name
function removeAnimationNames() {
// anonymize identifiers used for animation-name globally
if(CSSUsageResults.props["animation-name"]) {
CSSUsageResults.props["animation-name"].values = {"<custom-ident>":CSSUsageResults.props["animation-name"].count};
}
// anonymize identifiers used for animation-name per selector
for(var selector in CSSUsageResults.rules) {
var rule = CSSUsageResults.rules[selector];
if(rule && rule.props && rule.props["animation-name"]) {
rule.props["animation-name"].values = {"<custom-ident>":rule.props["animation-name"].count};
}
}
}
removeAnimationNames();
}
//-------------------------------------------------------------------------
/**
* If you try to do querySelectorAll on pseudo selectors
* it returns 0 because you are not actually doing the action the pseudo is stating those things,
* but we will honor those declarations and we don't want them to be missed,
* so we remove the pseudo selector from the selector text
*/
function cleanSelectorText(text) {
if(text.indexOf(':') == -1) {
return text;
} else {
return text.replace(/([-_a-zA-Z0-9*\[\]]?):(?:hover|active|focus|before|after|not\(:(hover|active|focus)\))|::(?:before|after)/gi, '>>$1<<').replace(/(^| |>|\+|~)>><</g,'$1*').replace(/\(>><<\)/g,'(*)').replace(/>>([-_a-zA-Z0-9*\[\]]?)<</g,'$1');
}
}
/**
* Returns an anonymized version of the selector.
* @example "#menu.open:hover>a.submenu" => "#id.class:hover > a.class"
*/
function generalizedSelectorsOf(value) {
// Trim
value = value.trim();
// Collapse whitespace
if (value) {
value = value.replace(/\s+/g, " ");
}
// Remove (...)
if (value.indexOf("(") != -1) {
value = value.replace(/[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]*)[)])*[)])*[)])*[)])*[)]/g, "");
value = value.replace(/[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]+|[(](?:[^()]*)[)])*[)])*[)])*[)])*[)]/g, "");
}
// Simplify "..." and '...'
value = value.replace(/"([^"\\]|\\[^"\\]|\\\\|\\")*"/g,'""')
value = value.replace(/'([^'\\]|\\[^'\\]|\\\\|\\')*'/g,"''");
// Simplify [att]
if (value.indexOf("[") != -1) {
value = value.replace(/\[[^=\[\]]+="([^"\\]|\\[^"\\]|\\\\|\\")*"\]/g, "[a]");
value = value.replace(/\[[^=\[\]]+='([^'\\]|\\[^'\\]|\\\\|\\')*'\]/g, "[a]");
value = value.replace(/\[[^\[\]]+\]/g, "[a]");
}
// Simplify .class
if (value.indexOf(".") != -1) {
value = value.replace(/[.][-_a-zA-Z][-_a-zA-Z0-9]*/g, ".c");
}
// Simplify #id
if (value.indexOf("#") != -1) {
value = value.replace(/[#][-_a-zA-Z][-_a-zA-Z0-9]*/g, "#i");
}
// Normalize combinators
value = value.replace(/[ ]*([>|+|~])[ ]*/g,' $1 ');
// Trim whitespace
value = value.trim();
// Remove unnecessary * to match Chrome
value = value.replace(/[*]([#.\x5B:])/g,'$1');
// Now we can sort components so that all browsers give results similar to Chrome
value = sortSelectorComponents(value)
// Split multiple selectors
value = value.split(/\s*,\s*/g);
return value;
}
var ID_REGEXP = "[#]i"; // #id
var CLASS_REGEXP = "[.]c"; // .class
var ATTR_REGEXP = "\\[a\\]"; // [att]
var PSEUDO_REGEXP = "[:][:]?[-_a-zA-Z][-_a-zA-Z0-9]*"; // :pseudo
var SORT_REGEXPS = [
// #id first
new RegExp("("+CLASS_REGEXP+")("+ID_REGEXP+")",'g'),
new RegExp("("+ATTR_REGEXP+")("+ID_REGEXP+")",'g'),
new RegExp("("+PSEUDO_REGEXP+")("+ID_REGEXP+")",'g'),
// .class second
new RegExp("("+ATTR_REGEXP+")("+CLASS_REGEXP+")",'g'),
new RegExp("("+PSEUDO_REGEXP+")("+CLASS_REGEXP+")",'g'),
// [attr] third
new RegExp("("+PSEUDO_REGEXP+")("+ATTR_REGEXP+")",'g'),
// :pseudo last
];
function sortSelectorComponents(value) {
var oldValue; do { // Yeah this is a very inefficient bubble sort. I know.
oldValue = value;
for(var i = 0; i < SORT_REGEXPS.length; i++) {
var wrongPair = SORT_REGEXPS[i];
value = value.replace(wrongPair,'$2$1');
}
} while(oldValue != value); return value;
}
}();
//
// extracts valuable informations about selectors in use
//
void function() {
//
// To understand framework and general css usage, we collect stats about classes, ids and pseudos.
// Those objects have the following shape:
// {"hover":5,"active":1,"focus":2}
//
var cssPseudos = Object.create(null); // collect stats about which pseudo-classes and pseudo-elements are used in the css
var domClasses = Object.create(null); // collect stats about which css classes are found in the <... class> attributes of the dom
var cssClasses = Object.create(null); // collect stats about which css classes are used in the css
var domIds = Object.create(null); // collect stats about which ids are found in the <... id> attributes of the dom
var cssIds = Object.create(null); // collect stats about which ids are used in the css
//
// To understand Modernizer usage, we need to know how often some classes are used at the front of a selector
// While we're at it, the code also collect the state for ids
//
var cssLonelyIdGates = Object.create(null); // .class something-else ==> {"class":1}
var cssLonelyClassGates = Object.create(null); // #id something-else ==> {"id":1}
var cssLonelyClassGatesMatches = []; // .class something-else ==> [".class something-else"]
var cssLonelyIdGatesMatches = []; // #id something-else ==> ["#id something-else"]
//
// These regular expressions catch patterns we want to track (see before)
//
var ID_REGEXP = /[#][-_a-zA-Z][-_a-zA-Z0-9]*/g; // #id
var ID_REGEXP1 = /[#][-_a-zA-Z][-_a-zA-Z0-9]*/; // #id (only the first one)
var CLASS_REGEXP = /[.][-_a-zA-Z][-_a-zA-Z0-9]*/g; // .class
var CLASS_REGEXP1 = /[.][-_a-zA-Z][-_a-zA-Z0-9]*/; // .class (only the first one)
var PSEUDO_REGEXP = /[:][-_a-zA-Z][-_a-zA-Z0-9]*/g; // :pseudo (only the )
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 ...
var GATECLASS_REGEXP = /^\s*[.][-_a-zA-Z][-_a-zA-Z0-9]*([:][-_a-zA-Z][-_a-zA-Z0-9]*)*\s+[^>+{, ][^{,]+$/; // .class ...
/**
* From a css selector text and a set of counters,
* increment the counters for the matches in the selector of the 'feature' regular expression
*/
function extractFeature(feature, selector, counters) {
var instances = selector.match(feature)||[];
for(var i = 0; i < instances.length; i++) {
var instance = instances[i];
instance = instance.substr(1);
counters[instance] = (counters[instance]|0) + 1;
}
}
/**
* This analyzer will collect over the selectors the stats defined before
*/
CSSUsage.SelectorAnalyzer = function parseSelector(style, selectorsText) {
if(typeof selectorsText != 'string') return;
var selectors = selectorsText.split(',');
for(var i = selectors.length; i--;) { var selector = selectors[i];
// extract all features from the selectors
extractFeature(ID_REGEXP, selector, cssIds);
extractFeature(CLASS_REGEXP, selector, cssClasses);
extractFeature(PSEUDO_REGEXP, selector, cssPseudos);
// detect specific selector patterns we're interested in
if(GATEID_REGEXP.test(selector)) {
cssLonelyIdGatesMatches.push(selector);
extractFeature(ID_REGEXP1, selector, cssLonelyIdGates);
}
if(GATECLASS_REGEXP.test(selector)) {
cssLonelyClassGatesMatches.push(selector);
extractFeature(CLASS_REGEXP1, selector, cssLonelyClassGates);
}
}
}
/**
* This analyzer will collect over the dom elements the stats defined before
*/
CSSUsage.DOMClassAnalyzer = function(element) {
// collect classes used in the wild
if(element.className) {
var elementClasses = element.classList;
for(var cl = 0; cl < elementClasses.length; cl++) {
var c = elementClasses[cl];
domClasses[c] = (domClasses[c]|0) + 1;
}
}
// collect ids used in the wild
if(element.id) {
domIds[element.id] = (domIds[element.id]|0) + 1;
}
}
/**
* This function will be called when all stats have been collected
* at which point we will agregate some of them in useful values like Bootstrap usages, etc...
*/
CSSUsage.SelectorAnalyzer.finalize = function() {
// get arrays of the classes/ids used ({"hover":5} => ["hover"]))
var domClassesArray = Object.keys(domClasses);
var cssClassesArray = Object.keys(cssClasses);
var domIdsArray = Object.keys(domIds);
var cssIdsArray = Object.keys(cssIds);
var results = {
// how many crawls are aggregated in this file (one of course in this case)
SuccessfulCrawls: 1,
// how many elements on the page (used to compute percentages for css.props)
DOMElements: document.all.length,
// how many selectors vs inline style, and other usage stats
SelectorsFound: CSSUsage.StyleWalker.amountOfSelectors,
InlineStylesFound: CSSUsage.StyleWalker.amountOfInlineStyles,
SelectorsUnused: CSSUsage.StyleWalker.amountOfSelectorsUnused,
// ids stats
IdsUsed: domIdsArray.length,
IdsRecognized: Object.keys(cssIds).length,
IdsUsedRecognized: filter(domIdsArray, i => cssIds[i]).length,
// classes stats
ClassesUsed: domClassesArray.length,
ClassesRecognized: Object.keys(cssClasses).length,
ClassesUsedRecognized: filter(domClassesArray, c => cssClasses[c]).length,
};
results = getFwkUsage(results, cssLonelyClassGates, domClasses, domIds, cssLonelyIdGates, cssClasses);
results = getPatternUsage(results, domClasses, cssClasses);
CSSUsageResults.usages = results;
deleteDuplicatedAtRules(); // TODO: issue #52
if(window.debugCSSUsage) if(window.debugCSSUsage) console.log(CSSUsageResults.usages);
}
/**
* Removes duplicated at rules data that was generated under CSSUsageResults.rules
* TODO: should not be using such a function, refer to issue #52
*/
function deleteDuplicatedAtRules() {
var cssUsageRules = CSSUsageResults.rules;
var keys = Object.keys(cssUsageRules);
for(let key of keys) {
// only remove specific atrules
if (key.includes("atrule:")) {
delete cssUsageRules[key];
}
}
delete CSSUsageResults.atrules["@atrule:8"]; // delete duplicated data from atrule:7, keyframe
}
}();
} catch (ex) { /* do something maybe */ throw ex; } }();
/*
RECIPE: File Input Usage
-------------------------------------------------------------
Author: Greg Whitworth
*/
void function() {
window.CSSUsage.StyleWalker.recipesToRun.push( function fileInputUsage(/*HTML DOM Element*/ element, results) {
if(element.nodeName == "INPUT") {
if (element.attributes.length > 0) {
for(var n = 0; n < element.attributes.length; n++) {
if(element.attributes[n].name == "type") {
if (element.attributes[n].value.toLowerCase() === "file") {
results["file"] = results["file"] || { count: 0 };
results["file"].count++;
}
}
}
}
}
return results;
});
}();
//
// This file is only here to create the TSV
// necessary to collect the data from the crawler
//
void function() {
/* String hash function
/* credits goes to http://erlycoder.com/49/javascript-hash-functions-to-convert-string-into-integer-hash- */
const hashCodeOf = (str) => {
var hash = 5381; var char = 0;
for (var i = 0; i < str.length; i++) {
char = str.charCodeAt(i);
hash = ((hash << 5) + hash) + char;
}
return hash;
}
var ua = navigator.userAgent;
var uaName = ua.indexOf('Edge')>=0 ? 'EDGE' :ua.indexOf('Chrome')>=0 ? 'CHROME' : 'FIREFOX';
window.INSTRUMENTATION_RESULTS = {
UA: uaName,
UASTRING: ua,
UASTRING_HASH: hashCodeOf(ua),
URL: location.href,
TIMESTAMP: Date.now(),
css: {/* see CSSUsageResults */},
html: {/* see HtmlUsageResults */},
dom: {},
scripts: {/* "bootstrap.js": 1 */},
};
window.INSTRUMENTATION_RESULTS_TSV = [];
/* make the script work in the context of a webview */
try {
var console = window.console || (window.console={log:function(){},warn:function(){},error:function(){}});
console.unsafeLog = console.log;
console.log = function() {
try {
this.unsafeLog.apply(this,arguments);
} catch(ex) {
// ignore
}
};
} catch (ex) {
// we tried...
}
}();
window.onCSSUsageResults = function onCSSUsageResults(CSSUsageResults) {
// Collect the results (css)
INSTRUMENTATION_RESULTS.css = CSSUsageResults;
INSTRUMENTATION_RESULTS.html = HtmlUsageResults;
INSTRUMENTATION_RESULTS.recipe = RecipeResults;
// Convert it to a more efficient format
INSTRUMENTATION_RESULTS_TSV = convertToTSV(INSTRUMENTATION_RESULTS);
// Remove tabs and new lines from the data
for(var i = INSTRUMENTATION_RESULTS_TSV.length; i--;) {
var row = INSTRUMENTATION_RESULTS_TSV[i];
for(var j = row.length; j--;) {
row[j] = (''+row[j]).replace(/(\s|\r|\n)+/g, ' ');
}
}
// Convert into one signle tsv file
var tsvString = INSTRUMENTATION_RESULTS_TSV.map((row) => (row.join('\t'))).join('\n');
appendTSV(tsvString);
// Add it to the document dom
function appendTSV(content) {
if(window.debugCSSUsage) console.log("Trying to append");
var output = document.createElement('script');
output.id = "css-usage-tsv-results";
output.textContent = tsvString;
output.type = 'text/plain';
document.querySelector('head').appendChild(output);
var successfulAppend = checkAppend();
}
function checkAppend() {
if(window.debugCSSUsage) if(window.debugCSSUsage) console.log("Checking append");
var elem = document.getElementById('css-usage-tsv-results');
if(elem === null) {
if(window.debugCSSUsage) console.log("Element not appended");
if(window.debugCSSUsage) console.log("Trying to append again");
appendTSV();
}
else {
if(window.debugCSSUsage) console.log("Element successfully found");
}
}
/** convert the instrumentation results to a spreadsheet for analysis */
function convertToTSV(INSTRUMENTATION_RESULTS) {
if(window.debugCSSUsage) console.log("Converting to TSV");
var VALUE_COLUMN = 4;
var finishedRows = [];
var currentRowTemplate = [
INSTRUMENTATION_RESULTS.UA,
INSTRUMENTATION_RESULTS.UASTRING_HASH,
INSTRUMENTATION_RESULTS.URL,
INSTRUMENTATION_RESULTS.TIMESTAMP,
0
];
currentRowTemplate.push('recipe');
convertToTSV(INSTRUMENTATION_RESULTS['recipe']);
currentRowTemplate.pop();
var l = finishedRows[0].length;
finishedRows.sort((a,b) => {
for(var i = VALUE_COLUMN+1; i<l; i++) {
if(a[i]<b[i]) return -1;
if(a[i]>b[i]) return +1;
}
return 0;
});
return finishedRows;
/** helper function doing the actual conversion */
function convertToTSV(object) {
if(object==null || object==undefined || typeof object == 'number' || typeof object == 'string') {
finishedRows.push(new Row(currentRowTemplate, ''+object));
} else {
for(var key in object) {
if({}.hasOwnProperty.call(object,key)) {
currentRowTemplate.push(key);
convertToTSV(object[key]);
currentRowTemplate.pop();
}
}
}
}
/** constructor for a row of our table */
function Row(currentRowTemplate, value) {
// Initialize an empty row with enough columns
var row = [
/*UANAME: edge */'',
/*UASTRING: mozilla/5.0 (...) */'',
/*URL: http://.../... */'',
/*TIMESTAMP: 1445622257303 */'',
/*VALUE: 0|1|... */'',
/*DATATYPE: css|dom|html... */'',
/*SUBTYPE: props|types|api|... */'',
/*NAME: font-size|querySelector|... */'',
/*CONTEXT: count|values|... */'',
/*SUBCONTEXT: px|em|... */'',
/*... */'',
/*... */'',
];
// Copy the column values from the template
for(var i = currentRowTemplate.length; i--;) {
row[i] = currentRowTemplate[i];
}
// Add the value to the row
row[VALUE_COLUMN] = value;
return row;
}
}
};
//
// Execution scheduler:
// This is where we decide what to run, and when
//
void function() {
var browserIsEdge = navigator.userAgent.indexOf('Edge')>=0;
var browserIsFirefox = navigator.userAgent.indexOf('Firefox')>=0;
if(document.readyState !== 'complete') {
// if the document is loading, run when it loads or in 10s, whichever is less
window.addEventListener('load', onready);
setTimeout(onready, 10000);
} else {
// if the document is ready, run now
onready();
}
/**
* This is the main entrypoint of our script
*/
function onready() {
// Uncomment if you want to set breakpoints when running in the console
//debugger;
// Prevent this code from running multiple times
var firstTime = !onready.hasAlreadyRun; onready.hasAlreadyRun = true;
if(!firstTime) { return; /* for now... */ }
// Prevent this code from running when the page has no stylesheet (probably a redirect page)
if(document.styleSheets.length == 0) { return; }
// Check to see if you're on a Firefox failure page
if(document.styleSheets.length == 1 && browserIsFirefox) {
if(document.styleSheets[0].href !== null && document.styleSheets[0].href.indexOf('aboutNetError') != -1) {
return;
}
}
// Keep track of duration
var startTime = performance.now();
// register tools
CSSUsage.StyleWalker.ruleAnalyzers.push(CSSUsage.PropertyValuesAnalyzer);
CSSUsage.StyleWalker.ruleAnalyzers.push(CSSUsage.SelectorAnalyzer);
CSSUsage.StyleWalker.elementAnalyzers.push(CSSUsage.DOMClassAnalyzer);
CSSUsage.StyleWalker.elementAnalyzers.push(HtmlUsage.GetNodeName);
// perform analysis
CSSUsage.StyleWalker.walkOverDomElements();
CSSUsage.StyleWalker.walkOverCssStyles();
CSSUsage.PropertyValuesAnalyzer.finalize();
CSSUsage.SelectorAnalyzer.finalize();
// Walk over the dom elements again for Recipes
CSSUsage.StyleWalker.runRecipes = true;
CSSUsage.StyleWalker.walkOverDomElements();
// Update duration
CSSUsageResults.duration = (performance.now() - startTime)|0;
// DO SOMETHING WITH THE CSS OBJECT HERE
window.debugCSSUsage = false;
if(window.onCSSUsageResults) {
window.onCSSUsageResults(CSSUsageResults);
}
}
}();