analyze-css
Advanced tools
Comparing version 1.1.2 to 2.0.0
@@ -6,7 +6,16 @@ /** | ||
function collection() { | ||
/** | ||
* @class | ||
*/ | ||
function Collection() { | ||
this.items = {}; | ||
} | ||
collection.prototype = { | ||
Collection.prototype = { | ||
/** | ||
* Pushes a given item to the collection and counts each occurrence | ||
* | ||
* @param {string} item | ||
* @return {void} | ||
*/ | ||
push: function (item) { | ||
@@ -22,2 +31,7 @@ if (typeof this.items[item] === "undefined") { | ||
/** | ||
* Sorts collected items in desending order by their occurrences | ||
* | ||
* @return {Collection} | ||
*/ | ||
sort: function () { | ||
@@ -43,2 +57,10 @@ var newItems = {}, | ||
/** | ||
* Runs provided callback for each item in the collection. | ||
* | ||
* Item and the count is provided to the callback. | ||
* | ||
* @param {forEachCallback} callback | ||
* | ||
*/ | ||
forEach: function (callback) { | ||
@@ -51,2 +73,8 @@ Object.keys(this.items).forEach(function (key) { | ||
module.exports = collection; | ||
/** | ||
* @callback forEachCallback | ||
* @param {string} item | ||
* @param {number} count | ||
*/ | ||
module.exports = Collection; |
415
lib/index.js
@@ -6,379 +6,88 @@ /** | ||
var cssParser = require("css").parse, | ||
debug = require("debug")("analyze-css"), | ||
fs = require("fs"), | ||
const debug = require("debug")("analyze-css"), | ||
path = require("path"), | ||
preprocessors = new (require("./preprocessors"))(), | ||
slickParse = require("slick").parse, | ||
VERSION = require("./../package").version; | ||
function analyzer(css, options, callback) { | ||
var res; | ||
function error(msg, code) { | ||
var err = new Error(msg); | ||
err.code = code; | ||
return err; | ||
} | ||
// Promise-based public endpoint | ||
function analyze(css, options) { | ||
// options can be omitted | ||
if (typeof options === "function") { | ||
callback = options; | ||
options = {}; | ||
} | ||
options = options || {}; | ||
this.options = options; | ||
debug("opts: %j", this.options); | ||
debug("opts: %j", options); | ||
if (typeof css !== "string") { | ||
callback( | ||
this.error( | ||
"css parameter passed is not a string!", | ||
analyzer.EXIT_CSS_PASSED_IS_NOT_STRING | ||
), | ||
null | ||
); | ||
return; | ||
} | ||
// preprocess the CSS (issue #3) | ||
if (typeof options.preprocessor === "string") { | ||
debug('Using "%s" preprocessor', options.preprocessor); | ||
var preprocessor = preprocessors.get(options.preprocessor); | ||
try { | ||
css = preprocessor.process(css, options); | ||
} catch (ex) { | ||
throw new Error("Preprocessing failed: " + ex); | ||
return new Promise((resolve, reject) => { | ||
if (typeof css !== "string") { | ||
reject( | ||
error( | ||
"css parameter passed is not a string!", | ||
analyze.EXIT_CSS_PASSED_IS_NOT_STRING | ||
) | ||
); | ||
return; | ||
} | ||
debug("Preprocessing completed"); | ||
} | ||
// preprocess the CSS (issue #3) | ||
if (typeof options.preprocessor === "string") { | ||
debug('Using "%s" preprocessor', options.preprocessor); | ||
res = this.analyze(css); | ||
var preprocessor = preprocessors.get(options.preprocessor); | ||
// error handling | ||
if (res !== true) { | ||
callback(res, null); | ||
return; | ||
} | ||
try { | ||
css = preprocessor.process(css, options); | ||
} catch (ex) { | ||
throw new Error("Preprocessing failed: " + ex); | ||
} | ||
// return the results | ||
res = { | ||
generator: "analyze-css v" + VERSION, | ||
metrics: this.metrics, | ||
}; | ||
// disable offenders output if requested (issue #64) | ||
if (options.noOffenders !== true) { | ||
res.offenders = this.offenders; | ||
} | ||
callback(null, res); | ||
} | ||
analyzer.version = VERSION; | ||
// @see https://github.com/macbre/phantomas/issues/664 | ||
analyzer.path = path.normalize(__dirname + "/.."); | ||
analyzer.pathBin = analyzer.path + "/bin/analyze-css.js"; | ||
// exit codes | ||
analyzer.EXIT_NEED_OPTIONS = 2; | ||
analyzer.EXIT_PARSING_FAILED = 251; | ||
analyzer.EXIT_EMPTY_CSS = 252; | ||
analyzer.EXIT_CSS_PASSED_IS_NOT_STRING = 253; | ||
analyzer.EXIT_URL_LOADING_FAILED = 254; | ||
analyzer.EXIT_FILE_LOADING_FAILED = 255; | ||
analyzer.prototype = { | ||
emitter: false, | ||
tree: false, | ||
metrics: {}, | ||
offenders: {}, | ||
error: function (msg, code) { | ||
var err = new Error(msg); | ||
err.code = code; | ||
return err; | ||
}, | ||
// emit given event | ||
emit: function (/* eventName, arg1, arg2, ... */) { | ||
//debug('Event %s emitted', arguments[0]); | ||
this.emitter.emit.apply(this.emitter, arguments); | ||
}, | ||
// bind to a given event | ||
on: function (ev, fn) { | ||
this.emitter.on(ev, fn); | ||
}, | ||
setMetric: function (name, value) { | ||
value = value || 0; | ||
//debug('setMetric(%s) = %d', name, value); | ||
this.metrics[name] = value; | ||
}, | ||
// increements given metric by given number (default is one) | ||
incrMetric: function (name, incr /* =1 */) { | ||
var currVal = this.metrics[name] || 0; | ||
incr = incr || 1; | ||
//debug('incrMetric(%s) += %d', name, incr); | ||
this.setMetric(name, currVal + incr); | ||
}, | ||
addOffender: function (metricName, msg, position /* = undefined */) { | ||
if (typeof this.offenders[metricName] === "undefined") { | ||
this.offenders[metricName] = []; | ||
debug("Preprocessing completed"); | ||
} | ||
this.offenders[metricName].push({ | ||
message: msg, | ||
position: position || this.currentPosition, | ||
}); | ||
}, | ||
const CSSAnalyzer = require("./css-analyzer"); | ||
const instance = new CSSAnalyzer(options); | ||
const res = instance.analyze(css); | ||
setCurrentPosition: function (position) { | ||
this.currentPosition = position; | ||
}, | ||
initRules: function () { | ||
var debug = require("debug")("analyze-css:rules"), | ||
re = /\.js$/, | ||
rules = []; | ||
// init events emitter | ||
this.emitter = new (require("events").EventEmitter)(); | ||
this.emitter.setMaxListeners(200); | ||
// load all rules | ||
rules = fs | ||
.readdirSync(fs.realpathSync(__dirname + "/../rules/")) | ||
// filter out all non *.js files | ||
.filter(function (file) { | ||
return re.test(file); | ||
}) | ||
// remove file extensions to get just names | ||
.map(function (file) { | ||
return file.replace(re, ""); | ||
}); | ||
debug("Rules to be loaded: %s", rules.join(", ")); | ||
rules.forEach(function (name) { | ||
var rule = require("./../rules/" + name); | ||
rule(this); | ||
debug('"%s" loaded: %s', name, rule.description); | ||
}, this); | ||
}, | ||
fixCss: function (css) { | ||
// properly handle ; in @import URLs | ||
// see https://github.com/macbre/analyze-css/pull/322 | ||
// see https://github.com/reworkcss/css/issues/137 | ||
return css.replace(/@import url([^)]+["'])/, (match) => { | ||
return match.replace(/;/g, "%3B"); | ||
}); | ||
}, | ||
parseCss: function (css) { | ||
var debug = require("debug")("analyze-css:parser"); | ||
debug("Going to parse %s kB of CSS", (css.length / 1024).toFixed(2)); | ||
if (css.trim() === "") { | ||
return this.error("Empty CSS was provided", analyzer.EXIT_EMPTY_CSS); | ||
// error handling | ||
if (res instanceof Error) { | ||
debug("Rejecting a promise with an error: " + res); | ||
reject(res); | ||
return; | ||
} | ||
css = this.fixCss(css); | ||
// return the results | ||
let result = { | ||
generator: "analyze-css v" + VERSION, | ||
metrics: instance.metrics, | ||
}; | ||
this.tree = cssParser(css, { | ||
// errors are listed in the parsingErrors property instead of being thrown (#84) | ||
silent: true, | ||
}); | ||
debug("CSS parsed"); | ||
return true; | ||
}, | ||
parseRules: function (rules) { | ||
const debug = require("debug")("analyze-css:parseRules"); | ||
rules.forEach(function (rule) { | ||
debug("%j", rule); | ||
// store the default current position | ||
// | ||
// it will be used when this.addOffender is called from within the rule | ||
// it can be overridden by providing a "custom" position via a call to this.setCurrentPosition | ||
this.setCurrentPosition(rule.position); | ||
switch (rule.type) { | ||
// { | ||
// "type":"media" | ||
// "media":"screen and (min-width: 1370px)", | ||
// "rules":[{"type":"rule","selectors":["#foo"],"declarations":[]}] | ||
// } | ||
case "media": | ||
this.emit("media", rule.media, rule.rules); | ||
// now run recursively to parse rules within the media query | ||
/* istanbul ignore else */ | ||
if (rule.rules) { | ||
this.parseRules(rule.rules); | ||
} | ||
this.emit("mediaEnd", rule.media, rule.rules); | ||
break; | ||
// { | ||
// "type":"rule", | ||
// "selectors":[".ui-header .ui-btn-up-a",".ui-header .ui-btn-hover-a"], | ||
// "declarations":[{"type":"declaration","property":"border","value":"0"},{"type":"declaration","property":"background","value":"none"}] | ||
// } | ||
case "rule": | ||
if (!rule.selectors || !rule.declarations) { | ||
return; | ||
} | ||
this.emit("rule", rule); | ||
// analyze each selector and declaration | ||
rule.selectors.forEach(function (selector) { | ||
var parsedSelector, | ||
expressions = [], | ||
i, | ||
len; | ||
// "#features > div:first-child" will become two expressions: | ||
// {"combinator":" ","tag":"*","id":"features"} | ||
// {"combinator":">","tag":"div","pseudos":[{"key":"first-child","value":null}]} | ||
parsedSelector = slickParse(selector)[0]; | ||
if (typeof parsedSelector === "undefined") { | ||
var positionDump = | ||
"Rule position start @ " + | ||
rule.position.start.line + | ||
":" + | ||
rule.position.start.column + | ||
", end @ " + | ||
rule.position.end.line + | ||
":" + | ||
rule.position.end.column; | ||
throw this.error( | ||
'Unable to parse "' + selector + '" selector. ' + positionDump, | ||
analyzer.EXIT_PARSING_FAILED | ||
); | ||
} | ||
// convert object with keys to array with numeric index | ||
for (i = 0, len = parsedSelector.length; i < len; i++) { | ||
expressions.push(parsedSelector[i]); | ||
} | ||
this.emit("selector", rule, selector, expressions); | ||
expressions.forEach(function (expression) { | ||
this.emit("expression", selector, expression); | ||
}, this); | ||
}, this); | ||
rule.declarations.forEach(function (declaration) { | ||
this.setCurrentPosition(declaration.position); | ||
switch (declaration.type) { | ||
case "declaration": | ||
this.emit( | ||
"declaration", | ||
rule, | ||
declaration.property, | ||
declaration.value | ||
); | ||
break; | ||
case "comment": | ||
this.emit("comment", declaration.comment); | ||
break; | ||
} | ||
}, this); | ||
break; | ||
// {"type":"comment","comment":" Cached as static-css-r518-9b0f5ab4632defb55d67a1d672aa31bd120f4414 "} | ||
case "comment": | ||
this.emit("comment", rule.comment); | ||
break; | ||
// {"type":"font-face","declarations":[{"type":"declaration","property":"font-family","value":"myFont"... | ||
case "font-face": | ||
this.emit("font-face", rule); | ||
break; | ||
// {"type":"import","import":"url('/css/styles.css')"} | ||
case "import": | ||
// replace encoded semicolon back into ; | ||
// https://github.com/macbre/analyze-css/pull/322 | ||
this.emit("import", rule.import.replace(/%3B/g, ";")); | ||
break; | ||
} | ||
}, this); | ||
}, | ||
run: function () { | ||
var stylesheet = this.tree && this.tree.stylesheet, | ||
rules = stylesheet && stylesheet.rules; | ||
this.emit("stylesheet", stylesheet); | ||
// check for parsing errors (#84) | ||
stylesheet.parsingErrors.forEach(function (err) { | ||
debug("error: %j", err); | ||
var pos = { | ||
line: err.line, | ||
column: err.column, | ||
}; | ||
this.setCurrentPosition({ | ||
start: pos, | ||
end: pos, | ||
}); | ||
this.emit("error", err); | ||
}, this); | ||
this.parseRules(rules); | ||
}, | ||
analyze: function (css) { | ||
var res, | ||
then = Date.now(); | ||
this.metrics = {}; | ||
this.offenders = {}; | ||
// load and init all rules | ||
this.initRules(); | ||
// parse CSS | ||
res = this.parseCss(css); | ||
if (res !== true) { | ||
return res; | ||
// disable offenders output if requested (issue #64) | ||
if (options.noOffenders !== true) { | ||
result.offenders = instance.offenders; | ||
} | ||
this.emit("css", css); | ||
debug("Promise resolved"); | ||
resolve(result); | ||
}); | ||
} | ||
// now go through parsed CSS tree and emit events for rules | ||
try { | ||
this.run(); | ||
} catch (ex) { | ||
return ex; | ||
} | ||
analyze.version = VERSION; | ||
this.emit("report"); | ||
// @see https://github.com/macbre/phantomas/issues/664 | ||
analyze.path = path.normalize(__dirname + "/.."); | ||
analyze.pathBin = analyze.path + "/bin/analyze-css.js"; | ||
debug("Completed in %d ms", Date.now() - then); | ||
return true; | ||
}, | ||
}; | ||
// exit codes | ||
analyze.EXIT_NEED_OPTIONS = 2; | ||
analyze.EXIT_PARSING_FAILED = 251; | ||
analyze.EXIT_EMPTY_CSS = 252; | ||
analyze.EXIT_CSS_PASSED_IS_NOT_STRING = 253; | ||
analyze.EXIT_URL_LOADING_FAILED = 254; | ||
analyze.EXIT_FILE_LOADING_FAILED = 255; | ||
module.exports = analyzer; | ||
module.exports = analyze; |
@@ -82,3 +82,5 @@ /** | ||
function analyze(css) { | ||
new analyzer(css, analyzerOpts, callback); | ||
analyzer(css, analyzerOpts) | ||
.then((res) => callback(null, res)) | ||
.catch((err) => callback(err, null)); | ||
} | ||
@@ -85,0 +87,0 @@ |
{ | ||
"name": "analyze-css", | ||
"version": "1.1.2", | ||
"version": "2.0.0", | ||
"author": "Maciej Brencz <maciej.brencz@gmail.com> (https://github.com/macbre)", | ||
"description": "CSS selectors complexity and performance analyzer", | ||
"main": "./lib/index.js", | ||
"types": "./lib/index.d.ts", | ||
"repository": { | ||
@@ -20,3 +21,3 @@ "type": "git", | ||
"engines": { | ||
"node": ">=10.0" | ||
"node": ">=14.0" | ||
}, | ||
@@ -28,2 +29,3 @@ "dependencies": { | ||
"css-shorthand-properties": "^1.1.1", | ||
"css-what": "^5.0.1", | ||
"debug": "^4.1.1", | ||
@@ -35,8 +37,9 @@ "fast-stats": "0.0.6", | ||
"onecolor": "^3.1.0", | ||
"slick": "~1.12.1", | ||
"specificity": "^0.4.1" | ||
}, | ||
"devDependencies": { | ||
"@types/css": "0.0.33", | ||
"autoprefixer": "^10.2.4", | ||
"browserslist": "^4.11.1", | ||
"check-dts": "^0.5.5", | ||
"eslint": "^7.12.1", | ||
@@ -47,3 +50,3 @@ "eslint-config-prettier": "8.3.0", | ||
"nyc": "^15.1.0", | ||
"postcss": "^8.2.6", | ||
"postcss": "^8.3.6", | ||
"prettier": "2.3.2" | ||
@@ -50,0 +53,0 @@ }, |
@@ -18,6 +18,12 @@ analyze-css | ||
```sh | ||
$ npm install --global analyze-css | ||
``` | ||
npm install --global analyze-css | ||
``` | ||
or to install from GitHub's repository: | ||
``` | ||
npm install --global @macbre/analyze-css | ||
``` | ||
## Usage | ||
@@ -53,9 +59,13 @@ | ||
``` | ||
npm i --save analyze-css | ||
``` | ||
```js | ||
var analyzer = require('analyze-css'); | ||
const analyze = require('analyze-css'); | ||
new analyzer('.foo {margin: 0 !important}', function(err, results) { | ||
console.error(err); | ||
(async() => { | ||
const results = await analyze('.foo {margin: 0 !important}'); | ||
console.log(results); // example? see below | ||
}); | ||
})(); | ||
``` | ||
@@ -65,10 +75,10 @@ | ||
// options can be provided | ||
var opts = { | ||
const opts = { | ||
'noOffenders': true | ||
}; | ||
new analyzer('.foo {margin: 0 !important}', opts, function(err, results) { | ||
console.error(err); | ||
(async() => { | ||
const results = await analyze('.foo {margin: 0 !important}', opts); | ||
console.log(results); // example? see below | ||
}); | ||
})(); | ||
``` | ||
@@ -75,0 +85,0 @@ |
@@ -6,2 +6,5 @@ "use strict"; | ||
/** | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
function rule(analyzer) { | ||
@@ -8,0 +11,0 @@ // @see http://stackoverflow.com/a/11335500 |
"use strict"; | ||
/** | ||
* @typedef { import("css-what").AttributeSelector[] } AttributeSelectors | ||
*/ | ||
/** | ||
* @param { AttributeSelectors } expressions | ||
* @returns { number } | ||
*/ | ||
function getBodyIndex(expressions) { | ||
let idx = 0; | ||
// body.foo h1 -> 0 | ||
// .foo body -> 1 | ||
// html.css body -> 1 | ||
for (let i = 0; i < expressions.length; i++) { | ||
switch (expressions[i].type) { | ||
case "tag": | ||
if (expressions[i].name === "body") { | ||
return idx; | ||
} | ||
break; | ||
case "child": | ||
case "descendant": | ||
idx++; | ||
} | ||
} | ||
return -1; | ||
} | ||
/** | ||
* @param { AttributeSelectors } expressions | ||
* @returns {boolean} | ||
*/ | ||
function firstSelectorHasClass(expressions) { | ||
// remove any non-class selectors | ||
return expressions[0].type === "tag" | ||
? // h1.foo | ||
expressions[1].type === "attribute" && expressions[1].name === "class" | ||
: // .foo | ||
expressions[0].type === "attribute" && expressions[0].name === "class"; | ||
} | ||
/** | ||
* @param { AttributeSelectors } expressions | ||
* @returns {number} | ||
*/ | ||
function getDescendantCombinatorIndex(expressions) { | ||
// body > .foo | ||
// {"type":"child"} | ||
return expressions | ||
.filter((item) => { | ||
return !["tag", "attribute", "pseudo"].includes(item.type); | ||
}) | ||
.map((item) => { | ||
return item.type; | ||
}) | ||
.indexOf("child"); | ||
} | ||
/** | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
function rule(analyzer) { | ||
const debug = require("debug")("analyze-css:bodySelectors"); | ||
analyzer.setMetric("redundantBodySelectors"); | ||
analyzer.on("selector", function (rule, selector, expressions) { | ||
var noExpressions = expressions.length; | ||
analyzer.on("selector", function (_, selector, expressions) { | ||
const noExpressions = expressions.length; | ||
@@ -14,15 +80,29 @@ // check more complex selectors only | ||
var firstTag = expressions[0].tag, | ||
firstHasClass = !!expressions[0].classList, | ||
isDescendantCombinator = expressions[1].combinator === ">", | ||
isShortExpression = noExpressions === 2, | ||
isRedundant = true; // always expect the worst ;) | ||
const firstTag = expressions[0].type === "tag" && expressions[0].name; | ||
const firstHasClass = firstSelectorHasClass(expressions); | ||
const isDescendantCombinator = | ||
getDescendantCombinatorIndex(expressions) === 0; | ||
// there only a single descendant / child selector | ||
// e.g. "body > foo" or "html h1" | ||
const isShortExpression = | ||
expressions.filter((item) => { | ||
return ["child", "descendant"].includes(item.type); | ||
}).length === 1; | ||
let isRedundant = true; // always expect the worst ;) | ||
// first, let's find the body tag selector in the expression | ||
var bodyIndex = expressions | ||
.map(function (item) { | ||
return item.tag; | ||
}) | ||
.indexOf("body"); | ||
const bodyIndex = getBodyIndex(expressions); | ||
debug("selector: %s %j", selector, { | ||
firstTag, | ||
firstHasClass, | ||
isDescendantCombinator, | ||
isShortExpression, | ||
bodyIndex, | ||
}); | ||
// body selector not found - skip the rules that follow | ||
@@ -34,3 +114,11 @@ if (bodyIndex < 0) { | ||
// matches "html > body" | ||
// {"type":"tag","name":"html","namespace":null} | ||
// {"type":"child"} | ||
// {"type":"tag","name":"body","namespace":null} | ||
// | ||
// matches "html.modal-popup-mode body" (issue #44) | ||
// {"type":"tag","name":"html","namespace":null} | ||
// {"type":"attribute","name":"class","action":"element","value":"modal-popup-mode","namespace":null,"ignoreCase":false} | ||
// {"type":"descendant"} | ||
// {"type":"tag","name":"body","namespace":null} | ||
if ( | ||
@@ -58,2 +146,4 @@ firstTag === "html" && | ||
if (isRedundant) { | ||
debug("selector %s - is redundant", selector); | ||
analyzer.incrMetric("redundantBodySelectors"); | ||
@@ -60,0 +150,0 @@ analyzer.addOffender("redundantBodySelectors", selector); |
"use strict"; | ||
/** | ||
* @param { import("css-what").Selector[] } expressions | ||
* @returns { number } | ||
*/ | ||
function getExpressionsLength(expressions) { | ||
// body -> 1 | ||
// ul li -> 2 | ||
// ol:lang(or) li -> 2 | ||
// .class + foo a -> 3 | ||
return ( | ||
expressions.filter((item) => { | ||
return ["child", "descendant", "adjacent"].includes(item.type); | ||
}).length + 1 | ||
); | ||
} | ||
/** | ||
* Report redundant child selectors, e.g.: | ||
* | ||
* ul li | ||
* ul > li | ||
* table > tr | ||
* tr td | ||
* | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
function rule(analyzer) { | ||
@@ -9,3 +35,3 @@ // definition of redundant child nodes selectors (see #51 for the initial idea): | ||
// table th | ||
var redundantChildSelectors = { | ||
const redundantChildSelectors = { | ||
ul: ["li"], | ||
@@ -20,53 +46,73 @@ ol: ["li"], | ||
analyzer.on("selector", function (rule, selector, expressions) { | ||
var noExpressions = expressions.length; | ||
analyzer.on("selector", (_, selector, expressions) => { | ||
// there only a single descendant / child selector | ||
// e.g. "body > foo" or "html h1" | ||
// | ||
// check more complex selectors only | ||
if (noExpressions < 2) { | ||
if (getExpressionsLength(expressions) < 3) { | ||
return; | ||
} | ||
// converts "ul#foo > li.test" selector into ['ul', 'li'] list | ||
var selectorNodeNames = expressions.map(function (item) { | ||
return item.tag; | ||
}); | ||
Object.keys(redundantChildSelectors).forEach((tagName) => { | ||
// find the tagName in our selector | ||
const tagInSelectorIndex = expressions | ||
.map((expr) => expr.type == "tag" && expr.name) | ||
.indexOf(tagName); | ||
Object.keys(redundantChildSelectors).forEach(function (nodeName) { | ||
var nodeIndex = selectorNodeNames.indexOf(nodeName), | ||
nextNode, | ||
curExpression, | ||
combinator, | ||
redundantNodes = redundantChildSelectors[nodeName]; | ||
// tag not found in the selector | ||
if (tagInSelectorIndex < 0) { | ||
return; | ||
} | ||
if (nodeIndex > -1 && nodeIndex < noExpressions - 1) { | ||
// skip cases like the following: "article > ul li" | ||
if (expressions[nodeIndex].combinator !== " ") { | ||
return; | ||
} | ||
// converts "ul#foo > li.test" selector into [{tag: 'ul'}, {combinator:'child'}, {tag: 'li'}] list | ||
const selectorNodeNames = expressions | ||
.filter((expr) => | ||
[ | ||
"tag", | ||
"descendant" /* */, | ||
"child" /* > */, | ||
"adjacent" /* + */, | ||
].includes(expr.type) | ||
) | ||
.map((expr) => | ||
expr.name ? { tag: expr.name } : { combinator: expr.type } | ||
); | ||
// we've found the possible offender, get the next node in the selector | ||
// and compare it against rules in redundantChildSelectors | ||
nextNode = selectorNodeNames[nodeIndex + 1]; | ||
// console.log(selector, expressions, selectorNodeNames); | ||
if (redundantNodes.indexOf(nextNode) > -1) { | ||
// skip selectors that match: | ||
// - by attributes - foo[class*=bar] | ||
// - by pseudo attributes - foo:lang(fo) | ||
curExpression = expressions[nodeIndex]; | ||
const tagIndex = selectorNodeNames | ||
.map((item) => item.tag) | ||
.indexOf(tagName); | ||
if (curExpression.pseudos || curExpression.attributes) { | ||
return; | ||
} | ||
const nextTagInSelector = selectorNodeNames[tagIndex + 2]?.tag; | ||
const nextCombinator = selectorNodeNames[tagIndex + 1]?.combinator; | ||
const previousCombinator = selectorNodeNames[tagIndex - 1]?.combinator; | ||
// only the following combinator can match: | ||
// ul li | ||
// ul > li | ||
combinator = expressions[nodeIndex + 1].combinator; | ||
// our tag is not followed by the tag listed in redundantChildSelectors | ||
const followedByRedundantTag = | ||
redundantChildSelectors[tagName].includes(nextTagInSelector); | ||
if (!followedByRedundantTag) { | ||
return; | ||
} | ||
if (combinator === " " || combinator === ">") { | ||
analyzer.incrMetric("redundantChildNodesSelectors"); | ||
analyzer.addOffender("redundantChildNodesSelectors", selector); | ||
} | ||
} | ||
// ignore cases like "article > ul li" | ||
if (previousCombinator === "child") { | ||
return; | ||
} | ||
// console.log( | ||
// tagName, {selector, expressions}, selectorNodeNames, | ||
// {tagIndex, prreviousTagInSelector, previousCombinator, nextTagInSelector, nextCombinator, followedByRedundantTag} | ||
// ); | ||
// only the following combinator can match: | ||
// ul li | ||
// ul > li | ||
if ( | ||
followedByRedundantTag && | ||
["descendant", "child"].includes(nextCombinator) | ||
) { | ||
analyzer.incrMetric("redundantChildNodesSelectors"); | ||
analyzer.addOffender("redundantChildNodesSelectors", selector); | ||
} | ||
}); | ||
@@ -73,0 +119,0 @@ }); |
@@ -18,2 +18,5 @@ "use strict"; | ||
/** | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
function rule(analyzer) { | ||
@@ -20,0 +23,0 @@ // store unique colors with the counter |
"use strict"; | ||
var format = require("util").format, | ||
const format = require("util").format, | ||
MAX_LENGTH = 256; | ||
/** | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
function rule(analyzer) { | ||
@@ -7,0 +10,0 @@ analyzer.setMetric("comments"); |
@@ -5,2 +5,5 @@ "use strict"; | ||
/** | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
function rule(analyzer) { | ||
@@ -7,0 +10,0 @@ analyzer.setMetric("complexSelectors"); |
"use strict"; | ||
var collection = require("../lib/collection"), | ||
const Collection = require("../lib/collection"), | ||
debug = require("debug")("analyze-css:duplicated"), | ||
format = require("util").format; | ||
/** | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
function rule(analyzer) { | ||
var selectors = new collection(), | ||
var selectors = new Collection(), | ||
mediaQueryStack = [], | ||
@@ -100,3 +103,3 @@ browserPrefixRegEx = /^-(moz|o|webkit|ms)-/; | ||
selectors.sort().forEach(function (selector, cnt) { | ||
selectors.sort().forEach((selector, cnt) => { | ||
if (cnt > 1) { | ||
@@ -103,0 +106,0 @@ analyzer.incrMetric("duplicatedSelectors"); |
"use strict"; | ||
/** | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
function rule(analyzer) { | ||
@@ -4,0 +7,0 @@ analyzer.setMetric("emptyRules"); |
@@ -5,2 +5,5 @@ "use strict"; | ||
/** | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
function rule(analyzer) { | ||
@@ -7,0 +10,0 @@ var re = /^expression/i; |
@@ -9,2 +9,3 @@ "use strict"; | ||
* @see http://browserhacks.com/ | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
@@ -11,0 +12,0 @@ function rule(analyzer) { |
"use strict"; | ||
/** | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
function rule(analyzer) { | ||
@@ -4,0 +7,0 @@ analyzer.setMetric("imports"); |
"use strict"; | ||
var format = require("util").format; | ||
const format = require("util").format; | ||
/** | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
function rule(analyzer) { | ||
@@ -6,0 +9,0 @@ analyzer.setMetric("importants"); |
"use strict"; | ||
/** | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
function rule(analyzer) { | ||
@@ -4,0 +7,0 @@ analyzer.on("css", function (css) { |
@@ -5,2 +5,5 @@ "use strict"; | ||
/** | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
function rule(analyzer) { | ||
@@ -7,0 +10,0 @@ analyzer.setMetric("mediaQueries"); |
@@ -5,2 +5,4 @@ "use strict"; | ||
* Detect not minified CSS | ||
* | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
@@ -12,2 +14,5 @@ function rule(analyzer) { | ||
* A simple CSS minification detector | ||
* | ||
* @param {string} css | ||
* @return {boolean} | ||
*/ | ||
@@ -22,3 +27,3 @@ function isMinified(css) { | ||
analyzer.on("css", function (css) { | ||
analyzer.on("css", (css) => { | ||
analyzer.setMetric("notMinified", isMinified(css) ? 0 : 1); | ||
@@ -25,0 +30,0 @@ }); |
"use strict"; | ||
/** | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
function rule(analyzer) { | ||
analyzer.setMetric("multiClassesSelectors"); | ||
analyzer.on("expression", function (selector, expression) { | ||
if (expression.classList && expression.classList.length > 1) { | ||
analyzer.on("selector", (_, selector, expressions) => { | ||
const expressionsWithClass = expressions.filter( | ||
(expr) => expr.name === "class" | ||
); | ||
// console.log(selector, expressions, {expressionsWithClass}); | ||
if (expressionsWithClass.length > 1) { | ||
analyzer.incrMetric("multiClassesSelectors"); | ||
analyzer.addOffender( | ||
"multiClassesSelectors", | ||
"." + expression.classList.join(".") | ||
"." + expressionsWithClass.map((expr) => expr.value).join(".") | ||
); | ||
@@ -13,0 +22,0 @@ } |
"use strict"; | ||
/** | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
function rule(analyzer) { | ||
analyzer.setMetric("parsingErrors"); | ||
analyzer.on("error", function (err) { | ||
analyzer.on("error", (err) => { | ||
analyzer.incrMetric("parsingErrors"); | ||
analyzer.addOffender("parsingErrors", err.reason); | ||
analyzer.addOffender("parsingErrors", err.message); | ||
}); | ||
@@ -10,0 +13,0 @@ } |
@@ -6,4 +6,7 @@ "use strict"; | ||
/** | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
function rule(analyzer) { | ||
var data = require("./prefixes.json"), | ||
var data = require(__dirname + "/prefixes.json"), | ||
prefixes = data.prefixes; | ||
@@ -10,0 +13,0 @@ |
@@ -10,2 +10,3 @@ "use strict"; | ||
* @see http://css-tricks.com/accidental-css-resets/ | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
@@ -12,0 +13,0 @@ function rule(analyzer) { |
"use strict"; | ||
/** | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
function rule(analyzer) { | ||
@@ -7,7 +10,9 @@ analyzer.setMetric("qualifiedSelectors"); | ||
// @see https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Writing_efficient_CSS | ||
analyzer.on("expression", function (selector, expression) { | ||
var hasId = expression.id, | ||
hasTag = expression.tag && expression.tag !== "*", | ||
hasClass = expression.classList; | ||
analyzer.on("selector", (_, selector, expressions) => { | ||
var hasId = expressions.some((expr) => expr.name === "id"), | ||
hasTag = expressions.some((expr) => expr.type === "tag"), | ||
hasClass = expressions.some((expr) => expr.name === "class"); | ||
// console.log(selector, expressions, {hasId, hasTag, hasClass}); | ||
if ( | ||
@@ -14,0 +19,0 @@ // tag#id |
@@ -7,2 +7,5 @@ "use strict"; | ||
/** | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
function rule(analyzer) { | ||
@@ -25,3 +28,3 @@ var types = ["Id", "Class", "Tag"], | ||
/* istanbul ignore if */ | ||
if (!selectorSpecificity) { | ||
if (!selectorSpecificity || !selectorSpecificity[0]) { | ||
debug("not counted for %s!", selector); | ||
@@ -28,0 +31,0 @@ return; |
"use strict"; | ||
/** | ||
* @param { import("../lib/css-analyzer") } analyzer | ||
*/ | ||
function rule(analyzer) { | ||
var selectors = 0, | ||
let selectors = 0, | ||
selectorsLength = 0; | ||
@@ -16,18 +19,23 @@ | ||
analyzer.on("rule", function () { | ||
analyzer.on("rule", () => { | ||
analyzer.incrMetric("rules"); | ||
}); | ||
analyzer.on("selector", function (rule, selector, expressions) { | ||
analyzer.on("selector", (_, __, expressions) => { | ||
selectors += 1; | ||
selectorsLength += expressions.length; | ||
selectorsLength += | ||
expressions.filter((item) => { | ||
return ["child", "descendant"].includes(item.type); | ||
}).length + 1; | ||
}); | ||
analyzer.on("declaration", function () { | ||
analyzer.on("declaration", () => { | ||
analyzer.incrMetric("declarations"); | ||
}); | ||
analyzer.on("expression", function (selector, expression) { | ||
analyzer.on("expression", (selector, expression) => { | ||
// console.log(selector, expression); | ||
// a[href] | ||
if (expression.attributes) { | ||
if (["exists"].includes(expression.action)) { | ||
analyzer.incrMetric("selectorsByAttribute"); | ||
@@ -37,8 +45,8 @@ } | ||
// .bar | ||
if (expression.classList) { | ||
if (expression.name === "class") { | ||
analyzer.incrMetric("selectorsByClass"); | ||
} | ||
// @foo | ||
if (expression.id) { | ||
// #foo | ||
if (expression.name === "id") { | ||
analyzer.incrMetric("selectorsById"); | ||
@@ -48,3 +56,3 @@ } | ||
// a:hover | ||
if (expression.pseudos) { | ||
if (expression.type === "pseudo") { | ||
analyzer.incrMetric("selectorsByPseudo"); | ||
@@ -54,3 +62,3 @@ } | ||
// header | ||
if (expression.tag && expression.tag !== "*") { | ||
if (expression.type === "tag") { | ||
analyzer.incrMetric("selectorsByTag"); | ||
@@ -60,3 +68,3 @@ } | ||
analyzer.on("report", function () { | ||
analyzer.on("report", () => { | ||
analyzer.setMetric("selectors", selectors); | ||
@@ -63,0 +71,0 @@ analyzer.setMetric("selectorLengthAvg", selectorsLength / selectors); |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
135822
36
4117
210
11
8
+ Addedcss-what@^5.0.1
+ Addedcss-what@5.1.0(transitive)
- Removedslick@~1.12.1
- Removedslick@1.12.2(transitive)