Last active
August 29, 2015 13:56
-
-
Save jonbretman/8828909 to your computer and use it in GitHub Desktop.
Look for bad CSS
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
/** | |
* CSSLinter Class | |
* @param {Object} options | |
* @constructor | |
*/ | |
var CSSLinter = function (options) { | |
var css = require('css'); | |
var fs = require('fs'); | |
this.file = options.file; | |
this.cssText = fs.readFileSync(this.file, 'utf8'); | |
this.stylesheet = css.parse(this.cssText).stylesheet; | |
this.selectors = {}; | |
this.selectorsArray = []; | |
this.mediaQueries = {}; | |
this.mediaQueryArray = []; | |
this.classes = {}; | |
this.classesArray = []; | |
this.ids = {}; | |
this.idsArray = []; | |
this.properties = {}; | |
this.emptyRules = []; | |
this.blocks = []; | |
this.processStylesheet(); | |
this.output(); | |
}; | |
CSSLinter.prototype = { | |
/** | |
* Process a stylesheet object returned from css.parse() | |
*/ | |
processStylesheet: function () { | |
this.stylesheet.rules.forEach(this.processRule.bind(this)); | |
this.selectorsArray = this.createSortedArray(this.selectors); | |
this.classesArray = this.createSortedArray(this.classes); | |
this.idsArray = this.createSortedArray(this.ids); | |
this.mediaQueryArray = this.createSortedArray(this.mediaQueries); | |
}, | |
/** | |
* Process a single rule, could be a rule or a media query | |
* @param {Object} rule | |
*/ | |
processRule: function (rule) { | |
if (rule.type === 'rule') { | |
// parse selectors | |
rule.selectors.forEach(this.processSelector.bind(this)); | |
// detect empty rules | |
if (!this.keyValueFilter(rule.declarations, 'type', 'declaration').length) { | |
this.emptyRules.push(rule.selectors); | |
} | |
else { | |
rule.declarations.forEach(this.processProperty.bind(this)); | |
this.blocks.push({ | |
declarations: rule.declarations, | |
selectors: rule.selectors | |
}); | |
} | |
} | |
// handle media query | |
if (rule.type === 'media') { | |
this.mediaQueries[rule.media] = this.increment(this.mediaQueries[rule.media]); | |
rule.rules.forEach(this.processRule.bind(this)); | |
} | |
}, | |
processSelector: function (selector) { | |
this.selectors[selector] = this.increment(this.selectors[selector]); | |
// classes | |
var match = selector.match(/\.[a-zA-Z0-9_\-]+/g); | |
if (match) { | |
match.forEach(function (c) { | |
this.classes[c] = this.increment(this.classes[c]); | |
}.bind(this)); | |
} | |
// ids | |
match = selector.match(/#[A-Za-z0-9_\-]+/g); | |
if (match) { | |
match.forEach(function (id) { | |
this.ids[id] = this.increment(this.ids[id]); | |
}.bind(this)); | |
} | |
}, | |
processProperty: function (property) { | |
var key = property.property + ':' + property.value; | |
this.properties[key] = this.increment(this.properties[key]); | |
}, | |
increment: function (n) { | |
return n ? n + 1 : 1; | |
}, | |
/** | |
* Return an array containing the longest selectors. | |
* @returns {Array[]} | |
*/ | |
getLongSelectors: function () { | |
// calculate average selector length | |
var averageLength = this.selectorsArray.reduce(function (length, selector) { | |
return length + selector.length; | |
}, 0) / this.selectorsArray.length; | |
// return array of all selectors that are longer than the average | |
return this.selectorsArray.filter(function (selector) { | |
return selector.length > averageLength; | |
}); | |
}, | |
/** | |
* Returns array of selectors sorted by their efficiency | |
* @returns {Array[]} | |
*/ | |
getInefficientSelectors: function () { | |
return this.selectorsArray.map(function (selector) { | |
// initial score | |
var score = 1; | |
// number of parts in the selector has quite low weight | |
score = score * ((selector.split(' ').length / 5) + 1); | |
// the number of different classes have a heigher weight | |
if (selector.match(/\.[a-zA-Z0-9_\-]+/g)) { | |
score = score * ((selector.match(/\.[a-zA-Z0-9_\-]+/g).length / 3) + 1); | |
} | |
// the number of ids have the highest weight | |
if (selector.match(/#[a-zA-Z0-9_\-]+/g)) { | |
score = score * selector.match(/#[a-zA-Z0-9_\-]+/g).length; | |
} | |
// weight is multiplies by the length of the selector | |
score = score * selector.length; | |
return { | |
selector: selector, | |
score: score | |
}; | |
}).sort(function (a, b) { | |
return b.score - a.score; | |
}).map(function (obj) { | |
return obj.selector + ' (score ' + obj.score + ')'; | |
}); | |
}, | |
getCommonBlocks: function () { | |
return this.blocks.map(function (block) { | |
block.matches = this.blocks.filter(function (comparison) { | |
var count = 0; | |
for (var i = 0; i < block.declarations.length; i++) { | |
for (var j = 0; j < comparison.declarations.length; j++) { | |
if (block.declarations[i].property === comparison.declarations[j].property && | |
block.declarations[i].value === comparison.declarations[j].value) { | |
count++; | |
} | |
} | |
} | |
return count >= 5; | |
}.bind(this)).map(function (block) { | |
return { | |
properties: block.declarations.map(function (d) { | |
return [d.property, d.value]; | |
}), | |
selectors: block.selectors | |
} | |
}); | |
return block; | |
}.bind(this)).sort(function (a, b) { | |
return b.matches.length - a.matches.length; | |
}).filter(function (block) { | |
return block.matches.length > 0; | |
}); | |
}, | |
/** | |
* Creates an array that is sorted based on a score derived from | |
* the length of the keys multiplied by the number of times they occured | |
* @param map | |
* @returns {Array} | |
*/ | |
createSortedArray: function (map) { | |
return Object.keys(map).map(function (value) { | |
return { | |
selector: value, | |
score: value.length * map[value] | |
}; | |
}).sort(function (a, b) { | |
return b.score - a.score; | |
}).map(function (obj) { | |
return obj.selector; | |
}); | |
}, | |
/** | |
* Returns an array where number of times each item is used is added to the end. | |
*/ | |
addUsedByCount: function (sortedArray, map) { | |
return sortedArray.map(function (value) { | |
return value + ' (used ' + map[value] + ' times)'; | |
}.bind(this)); | |
}, | |
/** | |
* | |
* @param {Object[]} arr | |
* @param {String} key | |
* @param {String} value | |
* @returns {Object[]} | |
*/ | |
keyValueFilter: function (arr, key, value) { | |
return arr.filter(function (obj) { | |
return obj[key] === value; | |
}); | |
}, | |
/** | |
* Outputs the results. | |
*/ | |
output: function () { | |
if (this.cmdOption('props')) { | |
console.log(this.addUsedByCount(this.createSortedArray(this.properties), this.properties).splice(0, 20)); | |
} | |
if (this.cmdOption('common')) { | |
console.log(JSON.stringify(this.getCommonBlocks())); | |
} | |
else { | |
this.printList('Long Selectors (eg. above average length)', this.addUsedByCount(this.getLongSelectors(), this.selectors)); | |
this.printList('Inefficient Selectors', this.getInefficientSelectors()); | |
this.printList('Most Used Classes', this.addUsedByCount(this.classesArray, this.classes)); | |
this.printList('Most Used ID\'s', this.addUsedByCount(this.idsArray, this.ids)); | |
this.printList('Media Queries', this.addUsedByCount(this.mediaQueryArray, this.mediaQueries)); | |
this.printList('Empty Rules', this.emptyRules); | |
this.printList('Most Used Properties', this.addUsedByCount(this.createSortedArray(this.properties), this.properties)); | |
} | |
}, | |
/** | |
* Prints an ordered list with a title. | |
* @param title | |
* @param items | |
*/ | |
printList: function (title, items) { | |
console.log(''); | |
console.log(title + ':'); | |
for (var i = 0; i < Math.min(items.length, 20); i++) { | |
console.log(' ' + (i + 1 < 10 ? ' ' + (i + 1) : i + 1) + ': ' + items[i]); | |
} | |
if (items.length > 20) { | |
console.log(' + ' + (items.length - 20) + ' more'); | |
} | |
console.log(''); | |
}, | |
cmdOption: function (option) { | |
return process.argv.indexOf('-' + option) !== -1; | |
} | |
}; | |
module.exports = CSSLinter; | |
new CSSLinter({ | |
file: 'media/css/lyst.css' | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment