Last active
July 27, 2017 15:26
-
-
Save campbellwmorgan/0f78a28d67fc885e80efaddf9349c2f0 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* This function analyses every DOM element | |
* that uses rules specified in the css stylesheet | |
* in the current document matching `cssFilename` | |
* | |
* @author Campbell Morgan, 2017 | |
* @example | |
* | |
* // 1. either load this code with the document or paste it into Chrome Dev Tools | |
* // 2. Instantiate the analyser | |
* | |
* const analyser = new CssAnalyser('yourCssFile'); | |
* | |
* // move around your app | |
* // then execute `analyser.run()` to collect data on current state | |
* analyser.run() | |
* // move around your app again and re-run if necessary | |
* analyser.run() | |
* | |
* // 3. When complete copy commented stylesheet | |
* copy(analyser.result()) | |
* | |
* // 4. (Optional) Pretty format css file to make more readable | |
* | |
* // The output will be | |
* .original_used_rule { margin: 20px } | |
* /** <element tag> - <element class attr> - <element xpath> ... | |
*/ | |
function CssAnalyser (cssFilename) { | |
if (!cssFilename) { | |
throw new Error("must specify a css filename as param"); | |
} | |
var self = this; | |
this.cssRuleList = {}; | |
this.matchingElements = {}; | |
this.run = styleSheetAnalysis; | |
this.result = getResult; | |
function clear () { | |
self.cssRuleList = {}; | |
self.matchingElements = {}; | |
} | |
function elementsByHash () { | |
let elByHash = {}; | |
for (let xPath in self.matchingElements) { | |
let el = self.matchingElements[xPath]; | |
el.rules.forEach((rule) => { | |
let elObj = { | |
xPath, | |
cls: el.cls, | |
type: el.type, | |
}; | |
if (elByHash[rule.cssText]) { | |
elByHash[rule.cssText].elements.push(elObj); | |
}else { | |
elByHash[rule.cssText] = { | |
classString: self.cssRuleList[rule.cssText], | |
elements: [elObj], | |
} | |
} | |
}); | |
} | |
return Object.values(elByHash); | |
} | |
function getResult () { | |
const cssStyles = elementsByHash(); | |
return cssStyles.reduce((output, style) => { | |
const elList = style.elements.reduce((els, element) => ( | |
els + ` ${element.type} - ${element.cls} - ${element.xPath} | |
` | |
), '/**') + ' **/'; | |
return output + ` | |
${style.classString} | |
${elList} | |
`; | |
}, ''); | |
} | |
// https://stackoverflow.com/questions/2952667/find-all-css-rules-that-apply-to-an-element | |
function getCSSRules(el) { | |
var sheets = document.styleSheets, ret = []; | |
el.matches = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector | |
|| el.msMatchesSelector || el.oMatchesSelector; | |
for (var i in sheets) { | |
if (sheets[i].href.indexOf(cssFilename) === -1 ) continue; | |
var rules = sheets[i].rules || sheets[i].cssRules; | |
for (var r in rules) { | |
if (el.matches(rules[r].selectorText)) { | |
ret.push(rules[r]); | |
} | |
} | |
} | |
return ret; | |
} | |
const filterCSSRules = (mapFn, rules) => { | |
if (!rules || !rules.length) return []; | |
return rules | |
.filter((rule) => ( // ignore rules that start with * | |
rule.selectorText.indexOf('*') !== 0 | |
)) | |
.map(mapFn); | |
} | |
const coreCSSRuleInfo = (cssRuleList) => (rule) => { | |
const hash = sum(rule.cssText); | |
if (!cssRuleList[hash]) cssRuleList[hash] = rule.cssText; | |
return { | |
selectorText: rule.selectorText, | |
cssText: hash, | |
} | |
} | |
function styleSheetAnalysis () { | |
let matchingElements = {}; | |
console.info('starting.... will take some time'); | |
document.querySelectorAll('*') | |
.forEach((el) => { | |
const rules = filterCSSRules( | |
coreCSSRuleInfo(self.cssRuleList), | |
getCSSRules(el) | |
); | |
if (rules && rules.length) { | |
const xPath = createXPathFromElement(el); | |
if (self.matchingElements[xPath]) { | |
self.matchingElements[xPath].rules = uniqueArray( | |
self.matchingElements[xPath].rules.concat(rules) | |
); | |
} else { | |
self.matchingElements[xPath] = { | |
type: el.nodeName, | |
cls: el.getAttribute('class'), | |
rules: rules || [], | |
}; | |
} | |
} | |
}); | |
console.info('Trawl Complete'); | |
return true; | |
} | |
function uniqueArray (arr) { | |
return arr.filter((val, index, self) => self.indexOf(val) === index); | |
} | |
// hash creator https://github.com/bevacqua/hash-sum/blob/master/hash-sum.js | |
function pad (hash, len) { | |
while (hash.length < len) { | |
hash = '0' + hash; | |
} | |
return hash; | |
} | |
function fold (hash, text) { | |
var i; | |
var chr; | |
var len; | |
if (text.length === 0) { | |
return hash; | |
} | |
for (i = 0, len = text.length; i < len; i++) { | |
chr = text.charCodeAt(i); | |
hash = ((hash << 5) - hash) + chr; | |
hash |= 0; | |
} | |
return hash < 0 ? hash * -2 : hash; | |
} | |
function foldObject (hash, o, seen) { | |
return Object.keys(o).sort().reduce(foldKey, hash); | |
function foldKey (hash, key) { | |
return foldValue(hash, o[key], key, seen); | |
} | |
} | |
function foldValue (input, value, key, seen) { | |
var hash = fold(fold(fold(input, key), toString(value)), typeof value); | |
if (value === null) { | |
return fold(hash, 'null'); | |
} | |
if (value === undefined) { | |
return fold(hash, 'undefined'); | |
} | |
if (typeof value === 'object') { | |
if (seen.indexOf(value) !== -1) { | |
return fold(hash, '[Circular]' + key); | |
} | |
seen.push(value); | |
return foldObject(hash, value, seen); | |
} | |
return fold(hash, value.toString()); | |
} | |
function toString (o) { | |
return Object.prototype.toString.call(o); | |
} | |
function sum (o) { | |
return pad(foldValue(0, o, '', []).toString(16), 8); | |
} | |
// https://stackoverflow.com/questions/2661818/javascript-get-xpath-of-a-node | |
function createXPathFromElement(elm) { | |
var allNodes = document.getElementsByTagName('*'); | |
for (var segs = []; elm && elm.nodeType == 1; elm = elm.parentNode) | |
{ | |
if (elm.hasAttribute('id')) { | |
var uniqueIdCount = 0; | |
for (var n=0;n < allNodes.length;n++) { | |
if (allNodes[n].hasAttribute('id') && allNodes[n].id == elm.id) uniqueIdCount++; | |
if (uniqueIdCount > 1) break; | |
}; | |
if ( uniqueIdCount == 1) { | |
segs.unshift('id("' + elm.getAttribute('id') + '")'); | |
return segs.join('/'); | |
} else { | |
segs.unshift(elm.localName.toLowerCase() + '[@id="' + elm.getAttribute('id') + '"]'); | |
} | |
} else if (elm.hasAttribute('class')) { | |
segs.unshift(elm.localName.toLowerCase() + '[@class="' + elm.getAttribute('class') + '"]'); | |
} else { | |
for (i = 1, sib = elm.previousSibling; sib; sib = sib.previousSibling) { | |
if (sib.localName == elm.localName) i++; }; | |
segs.unshift(elm.localName.toLowerCase() + '[' + i + ']'); | |
}; | |
}; | |
return segs.length ? '/' + segs.join('/') : null; | |
}; | |
return this; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment