Skip to content

Instantly share code, notes, and snippets.

@campbellwmorgan
Last active July 27, 2017 15:26
Show Gist options
  • Save campbellwmorgan/0f78a28d67fc885e80efaddf9349c2f0 to your computer and use it in GitHub Desktop.
Save campbellwmorgan/0f78a28d67fc885e80efaddf9349c2f0 to your computer and use it in GitHub Desktop.
/**
* 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