├── .gitignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── docs ├── .DS_Store ├── contributing │ └── readme.md ├── installation │ └── readme.md ├── metrics │ └── readme.md ├── readme.md ├── usage-package │ └── readme.md └── usage │ └── readme.md ├── lib ├── ArraySelectorSpecificity.js ├── ClassicalSelectorSpecificity.js ├── CliController.js ├── CliFormatter.js ├── CssDeclaration.js ├── CssMediaQuery.js ├── CssRule.js ├── CssSelector.js ├── CssStylesheet.js ├── Formatters.js ├── Info.js ├── Parker.js └── PreciseSelectorSpecificity.js ├── metrics ├── All.js ├── DeclarationsPerRule.js ├── IdentifiersPerSelector.js ├── MediaQueries.js ├── PreciseSpecificityPerSelector.js ├── PreciseTopSelectorSpecificity.js ├── SelectorsPerRule.js ├── SpecificityPerSelector.js ├── StringTopSelectorSpecificity.js ├── TopSelectorSpecificity.js ├── TopSelectorSpecificitySelector.js ├── TotalDeclarations.js ├── TotalIdSelectors.js ├── TotalIdentifiers.js ├── TotalImportantKeywords.js ├── TotalMediaQueries.js ├── TotalRules.js ├── TotalSelectors.js ├── TotalStylesheetSize.js ├── TotalStylesheets.js ├── TotalUniqueColours.js ├── UniqueColours.js └── ZIndexes.js ├── package.json ├── parker.js └── test ├── CliController.js ├── CssDeclaration.js ├── CssMediaQuery.js ├── CssRule.js ├── CssSelector.js ├── CssStylesheet.js ├── DeclarationsPerRule.js ├── IdentifiersPerSelector.js ├── Parker.js ├── TopSelectorSpecificity.js ├── TotalIdSelectors.js └── ZIndexes.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "node": true, 4 | "esnext": true, 5 | "bitwise": false, 6 | "curly": false, 7 | "eqeqeq": true, 8 | "eqnull": true, 9 | "immed": true, 10 | "latedef": false, 11 | "laxcomma": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "undef": true, 15 | "strict": true, 16 | "trailing": true, 17 | "smarttabs": true, 18 | "white": true 19 | } 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.12" 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | == HEAD 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This software is released under the MIT license: 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Parker 2 | 3 | Parker is a stylesheet analysis tool. It runs metrics on your stylesheets and will report on their complexity. 4 | 5 | [![Build Status](https://secure.travis-ci.org/katiefenn/parker.png?branch=master)](http://travis-ci.org/katiefenn/parker) 6 | 7 | 8 | ## Installation 9 | 10 | Install with npm: 11 | 12 | ``` 13 | npm install -g parker 14 | ``` 15 | 16 | ## Usage 17 | 18 | ### Measuring Local Stylesheets 19 | 20 | ``` 21 | parker a.css b.css c.css 22 | ``` 23 | ``` 24 | parker css/ 25 | ``` 26 | 27 | ### Measuring a Remote Stylesheet Using Curl 28 | 29 | ``` 30 | curl http://www.katiefenn.co.uk/css/shuttle.css -s | parker -s 31 | ``` 32 | 33 | ### Output JSON 34 | 35 | ``` 36 | parker example.css --format=json 37 | ``` 38 | 39 | ### Programmatic usage 40 | 41 | After installing parker as a dependency in your project, you can use it as follows: 42 | 43 | ```js 44 | var Parker = require('parker/lib/Parker'); 45 | var metrics = require('parker/metrics/All'); // Or an array of the metrics you want to measure 46 | 47 | var file = fs.readFile('test.css', function (err, data) { 48 | if (err) throw err; 49 | 50 | var parker = new Parker(metrics); 51 | var results = parker.run(data.toString()); 52 | 53 | // Do something with results 54 | }); 55 | ``` 56 | 57 | ## Documentation 58 | 59 | Documentation can be found in markdown format the [docs folder](https://github.com/katiefenn/parker/tree/master/docs). 60 | 61 | ## Testing 62 | 63 | From the repo root: 64 | 65 | ``` 66 | npm install 67 | npm test 68 | ``` 69 | 70 | ## Contributing 71 | 72 | Pull requests, issues, new unit tests, code reviews and good advice are all things that would make a difference to Parker. You can even contribute by telling me how useful Parker is to you; please let me know on Twitter at @katie_fenn. Any time generously donated to helping make Parker better is gratefully accepted, and in return I shall do my best to merge contributions. 73 | 74 | Please target pull requests at the "develop" branch. 75 | 76 | ## About 77 | 78 | Parker is my first open source project, and your suggestions and feedback are welcome. The project is in a pre-beta phase and is liable to change at any time. Parker is named for the character Parker from Gerry Anderson's Thunderbirds, without which my interest in technology and computers would certainly not be what it is today. Parker is Nosey about your stylesheets. 79 | -------------------------------------------------------------------------------- /docs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katiefenn/parker/7658d1b8741a6e7a5ee77b2ad21f7ecb8abab42c/docs/.DS_Store -------------------------------------------------------------------------------- /docs/contributing/readme.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Pull requests, issues, new unit tests, code reviews and good advice are all things that would make a difference to Parker. You can even contribute by telling me how useful Parker is to you; please let me know on Twitter at @katie_fenn. Any time generously donated to helping make Parker better is gratefully accepted, and in return I shall do my best to merge contributions. 3 | 4 | 5 | ## GitHub 6 | The source-code is hosted at [GitHub](https://github.com/katiefenn/Parker). 7 | 8 | When creating pull requests, please target the develop branch. 9 | 10 | 11 | ## Testing 12 | A suite of unit tests are maintained to test Parker. The tools Mocha, Chai and Sinon are among the tools used for testing. 13 | 14 | From the project root, dependencies can be installed using the following command: 15 | 16 | npm install 17 | 18 | Unit tests can be run using the following command: 19 | 20 | npm test 21 | -------------------------------------------------------------------------------- /docs/installation/readme.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | Parker requires Node.JS and npm to be installed before it can be installed itself. 3 | 4 | 5 | ## Install Node.JS 6 | Node.JS is a Javascript software platform for use outside of the web browser. It allows tools like Parker to be written in JavaScript and run from the command-line on your computer. 7 | 8 | npm is a package manager for Node.JS and is also installed by Node.JS installer packages. 9 | 10 | Installer packages for Node.JS can be found at the [Node.JS Website](http://nodejs.org/download/) for many operating systems. Node.JS can also be installed using a range of [package managers](https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager). 11 | 12 | 13 | ## Install Parker 14 | Install with npm: 15 | 16 | npm install -g parker -------------------------------------------------------------------------------- /docs/metrics/readme.md: -------------------------------------------------------------------------------- 1 | # Metrics 2 | Parker has a suite of metrics that are useful for measuring stylesheets. What follows is a list of all the metrics that are bundled with Parker. Parker's metrics are modular and it's easy to write your own - find out how [here](#authoring-metrics). 3 | 4 | 1. [Bundled Metrics](#bundled-metrics) 5 | 1. [Stylesheet Totals](#stylesheet-totals) 6 | 1. [Total Stylesheets](#total-stylesheets) 7 | 2. [Total Stylesheet Size](#total-stylesheet-size) 8 | 2. [Stylesheet Elements](#stylesheet-elements) 9 | 1. [Total Rules](#total-rules) 10 | 2. [Total Selectors](#total-selectors) 11 | 3. [Total Identifiers](#total-identifiers) 12 | 4. [Total Declarations](#total-declarations) 13 | 5. [Selectors Per Rule](#selectors-per-rule) 14 | 6. [Identifiers Per Selectors](#identifiers-per-selector) 15 | 3. [Specificity](#specificity) 16 | 1. [Specificity Per Selector](#specificity-per-selector) 17 | 2. [Top Selector Specificity](#top-selector-specificity) 18 | 3. [Top Selector Specificity Selector](#top-selector-specificity-selector) 19 | 4. [Important Keywords](#important-keywords) 20 | 1. [Total Important Keywords](#total-important-keywords) 21 | 5. [Colors](#colours) 22 | 1. [Total Unique Colours](#total-unique-colours) 23 | 2. [Unique Colours](#unique-colours) 24 | 6. [Media Queries](#media-queries-heading) 25 | 1. [Media Queries](#media-queries) 26 | 2. [Total Media Queries](#total-media-queries) 27 | 28 | 2. [Authoring Metrics](#authoring-metrics) 29 | 1. [Attributes](#attributes) 30 | 1. [id](#id) 31 | 2. [name](#name) 32 | 3. [type](#type) 33 | 4. [aggregate](#aggregate) 34 | 5. [format](#format) 35 | 2. [Methods](#methods) 36 | 1. [measure](#measure) 37 | 2. [filter](#filter) 38 | 3. [iterator](#iterator) 39 | 40 | 41 | ## [Bundled Metrics](#bundled-metrics) 42 | 43 | 44 | ### Stylesheet Totals 45 | Basic metrics that influence stylesheet and HTTP performance. 46 | 47 | 48 | #### Total Stylesheets 49 | The number of stylesheets measured. This is the number of stylesheets submitted to Parker in a single report, and can be used to track how many stylesheets are requested in your application. Each stylesheet is downloaded over a HTTP request, which affects performance. Generally, fewer HTTP requests improves performance. 50 | 51 | - __id__: total-stylesheets 52 | - __name__: Total Stylesheets 53 | - __type__: stylesheet 54 | - __aggregate__: sum 55 | - __format__: number 56 | 57 | 58 | #### Total Stylesheet Size 59 | Measures the total file size of stylesheets measured. Each stylesheet is downloaded over a HTTP request, and the size of the request affects performance. Smaller HTTP request file sizes improves performance. 60 | 61 | - __id__: total-stylesheet-size 62 | - __name__: Total Stylesheet Size 63 | - __type__: stylesheet 64 | - __aggregate__: sum 65 | - __format__: number 66 | 67 | 68 | ### Stylesheet Elements 69 | Stylesheet size can be broken down into the total number of its parts: rules, selectors, identifiers and declarations. 70 | 71 | 72 | #### Total Rules 73 | Measures the total number of rules. Each rule defines a specific behaviour of the design. Stylesheets with fewer rules are simpler. 74 | 75 | - __id__: total-rules 76 | - __name__: Total Rules 77 | - __type__: rule 78 | - __aggregate__: sum 79 | - __format__: number 80 | 81 | 82 | #### Total Selectors 83 | Measures the total number of selectors. Each selector defines a group of elements affected by the design. Stylesheets with fewer selectors are simpler. 84 | 85 | - __id__: total-selectors 86 | - __name__: Total Selectors 87 | - __type__: selector 88 | - __aggregate__: sum 89 | - __format__: number 90 | 91 | 92 | #### Total Identifiers 93 | Measures the total number of identifiers. Each identifier defines a group of elements affected by the design. Stylesheets with fewer identifiers are simpler. It can be useful to break measure identifiers as well as identifiers so that small code changes to selectors can be tracked. 94 | 95 | - __id__: total-identifiers 96 | - __name__: Total Identifiers 97 | - __type__: identifier 98 | - __aggregate__: sum 99 | - __format__: number 100 | 101 | 102 | #### Total Declarations 103 | Measures the total number of property declarations. Each property declaration defines a modification of the appearance or function of an element. Stylesheets with fewer property declarations are simpler. 104 | 105 | - __id__: total-declarations 106 | - __name__: Total Declarations 107 | - __type__: declaration 108 | - __aggregate__: sum 109 | - __format__: number 110 | 111 | #### Selectors Per Rule 112 | Measures the average number of selectors in every rule. Stylesheet rules can be applied to several groups of elements using multiple selectors, separated by a comma. Fewer selectors in a rule makes its properties specific to a smaller group of elements, and also makes a rule easier to read in text editors and developer tools. 113 | 114 | - __id__: total-important-keywords 115 | - __name__: Total Important Keywords 116 | - __type__: value 117 | - __aggregate__: sum 118 | - __format__: number 119 | 120 | #### Identifiers Per Selector 121 | Measures the average number of identifiers in every selector. Selectors can be made more specific to combinations of elements by adding more identifiers to a selector. Fewer identifiers in a given selector reduces its dependency on certain DOM structures, allowing more changes to your HTML before being forced to change your CSS. Selectors with fewer identifiers are also more readable. 122 | 123 | - __id__: identifiers-per-selector 124 | - __name__: Identifiers Per Selector 125 | - __type__: selector 126 | - __aggregate__: mean 127 | - __format__: number 128 | 129 | 130 | ### Specificity 131 | A rule can be overrided by another rule with a more specific selector. Complexity is added to stylesheets when multiple levels of cascading rules are used in stylesheets, because it becomes more difficult to predict which properties apply to a given element without keeping in mind other rules. 132 | 133 | 134 | #### Specificity Per Selector 135 | Measures the average specificity of selectors. Lower average specificity makes it easier to combine and re-use properties defined in other, less-specific rules. 136 | 137 | - __id__: specificity-per-selector 138 | - __name__: Specificity Per Selector 139 | - __type__: selector 140 | - __aggregate__: mean 141 | - __format__: number 142 | 143 | 144 | #### Top Selector Specificity 145 | Measures the specificity of the most specific selector. Reducing the specificity of the most complex selectors is a good way to reducing the overall complexity of a stylesheet. 146 | 147 | - __id__: top-selector-specificity 148 | - __name__: Top Selector Specificity 149 | - __type__: selector 150 | - __aggregate__: max 151 | - __format__: number 152 | 153 | 154 | #### Top Selector Specificity Selector 155 | Displays the most specific selector. Reducing the specificity of the most complex selectors is a good way to reducing the overall complexity of a stylesheet. 156 | 157 | - __id__: top-selector-specificity-selector 158 | - __name__: Top Selector Specificity Selector 159 | - __type__: selector 160 | - __aggregate__: max 161 | - __format__: string 162 | 163 | 164 | ### Important Keywords 165 | The !important keyword supersedes selector specificity, even allowing properties to be enforced over properties of rules with high specificity selectors. Because !important is so powerful, it should be reserved for use for architectural purposes, enforcing styles in user stylesheets and utility classes. Using !important to overcome issues with specificity exacerbates the problem. 166 | 167 | 168 | #### Total Important Keywords 169 | Measures the total instances of the !important keyword. Fewer !important keywords indicates a simpler stylesheet. 170 | 171 | - __id__: total-important-keywords 172 | - __name__: Total Important Keywords 173 | - __type__: value 174 | - __aggregate__: sum 175 | - __format__: number 176 | 177 | 178 | ### Colours 179 | Colour is an important part of design, and highlights important elements such as buttons, sections and text. A consistent colour scheme is a good way of guiding users around a site. An excessive number of colours indicates an overly-complex colour scheme, or inconsistent use of colour that forces an over-reliance of developers on design documents. 180 | 181 | 182 | #### Total Unique Colours 183 | Measures the number of unique colour hashes used in a stylesheet. Fewer colours indicates a simpler colour scheme. 184 | 185 | - __id__: total-unique-colours 186 | - __name__: Total Unique Colors 187 | - __type__: value 188 | - __aggregate__: length 189 | - __format__: number 190 | 191 | 192 | #### Unique Colours 193 | Lists the unique colour hashes used in a stylesheet. Identifying and reducing unique colours is a good way to simplify colour schemes. 194 | 195 | - __id__: unique-colours 196 | - __name__: Unique Colors 197 | - __type__: value 198 | - __aggregate__: list 199 | - __format__: list 200 | 201 | 202 | ### Media Queries 203 | Media queries contain rules that change the behaviour of documents according to the sort of device or its state. They're commonly used to create breakpoints in responsive web design behaviour. Each unique media query adds complexity by changing behaviour when a given criteria is met by the device. 204 | 205 | 206 | #### Media Queries 207 | Lists every unique media query used. Reducing unique media queries is a good way to simplify stylesheets. 208 | 209 | - __id__: media-queries 210 | - __name__: Media Queries 211 | - __type__: mediaquery 212 | - __aggregate__: list' 213 | - __format__: list 214 | 215 | 216 | #### Total Media Queries 217 | Measures the number of unique media queries used. Fewer media queries indicates a simpler stylesheet. 218 | 219 | - __id__: total-media-queries 220 | - __name__: Total Media Queries 221 | - __type__: mediaquery 222 | - __aggregate__: length 223 | - __format__: number 224 | 225 | 226 | ## Authoring Metrics 227 | Parker's stylesheets are modular, and there are several attributes and methods a metric can implement to access Parker's metric features. 228 | 229 | 230 | ### Attributes 231 | 232 | #### id 233 | The unique identifier of the metric and is used when selecting metrics to be run. 234 | 235 | 236 | #### name 237 | The natural language name of the metric and is used when reporting results. 238 | 239 | 240 | #### type 241 | The type of stylesheet elements the metric is run against. 242 | 243 | Available types: 244 | 245 | - stylesheet 246 | - rule 247 | - selector 248 | - identifier 249 | - declaration 250 | - property 251 | - value 252 | - mediaquery 253 | 254 | 255 | #### aggregate 256 | The operation applied to data collected by metrics on elements before results are reported. 257 | 258 | Available aggregates: 259 | 260 | - __sum__: will sum number values 261 | - __mean__: will average number values by mean 262 | - __max__: will select the maximum of number values after iterator function is applied 263 | - __list__: will list all collected values after filter function is applied 264 | - __length__: will display the number of collected values after filter function is applied 265 | 266 | 267 | #### format 268 | The output format of the end result. Used for selecting metrics for use in scripts. 269 | 270 | Available formats: 271 | 272 | - number 273 | - list 274 | - string 275 | 276 | 277 | ### Methods 278 | 279 | #### measure 280 | metric.measure(element) 281 | Measures an element of css "element" and returns a measurement to be reported. This is the main method that measures elements that all metrics must implement. 282 | 283 | Example: 284 | The Stylesheet Size measure method returns a simple number measurement, which is summed on aggregate. 285 | 286 | module.exports = { 287 | id: 'total-stylesheet-size', 288 | name: 'Total Stylesheet Size', 289 | type: 'stylesheet', 290 | aggregate: 'sum', 291 | format: 'number', 292 | measure: function (stylesheet) { 293 | return byteCount(stylesheet); 294 | } 295 | }; 296 | 297 | function byteCount(s) { 298 | return encodeURI(s).split(/%..|./).length - 1; 299 | } 300 | 301 | 302 | #### filter 303 | metric.filter(predicate) 304 | Filters list-aggregated results based on the truth-test "predicate" function. 305 | 306 | Predicate parameters: 307 | 308 | - __value__: the value of the measurement 309 | - __index__: the index of the item in all measurements the metric has collected 310 | - __list__: the current list of all measurements to compare the current item to 311 | 312 | Example: 313 | The Total Media Queries returns the query to be collected into a list. Before reporting, the filter method is run on the list, removing duplicate queries by returning a true or false value according to whether it is the only such item in the list. The length aggregate returns the number of queries collected after filtering. 314 | 315 | module.exports = { 316 | id: 'total-media-queries', 317 | name: 'Total Media Queries', 318 | type: 'mediaquery', 319 | aggregate: 'length', 320 | format: 'number', 321 | measure: function (query) { 322 | return query; 323 | }, 324 | filter: function (value, index, list) { 325 | return self.indexOf(value) === index; 326 | } 327 | }; 328 | 329 | 330 | #### iterator 331 | metric.iterator(element) 332 | A helper method of "max" aggregated metrics that returns a number value for the given element. Used to determine which item should be selected as the maximum value when the measurement returned by the measurement method is not a number value. When the method is not defined, the measurement returned by "measure" is used instead. 333 | 334 | Example: 335 | The Top Selector Specificity Selector metric returns the most specific selector, but does not report a number value. An iterator method is used to determine the selector's specificity when results are aggregated. 336 | 337 | module.exports = { 338 | id: 'top-selector-specificity-selector', 339 | name: 'Top Selector Specificity Selector', 340 | type: 'selector', 341 | aggregate: 'max', 342 | format: 'string', 343 | measure: function (selector) { 344 | return selector; 345 | }, 346 | iterator: function (selector) { 347 | var identifiers = getIdentifiers(selector), 348 | specificity = 0; 349 | 350 | /* 351 | Implementation calculating specificity cut for brevity 352 | ... 353 | */ 354 | 355 | return specificity; 356 | } 357 | }; 358 | 359 | 360 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # Parker 2 | Parker is a stylesheet analysis tool. It runs metrics on your stylesheets and will report on their complexity. 3 | 4 | ## Table of Contents 5 | 6 | 1. [Installation](installation/readme.md) 7 | 1. [Installing Node.JS](installation/readme.md#install-node-js) 8 | 2. [Installing Parker](installation/readme.md#install-parker) 9 | 2. [Usage](usage/readme.md) 10 | 1. [Options](usage/readme.md#options) 11 | 3. [Using Parker as a Package](usage-package/readme.md) 12 | 3. [Metrics](metrics/readme.md) 13 | 1. [Bundled Metrics](metrics/readme.md#bundled-metrics) 14 | 2. [Authoring Metrics](metrics/readme.md#authoring-metrics) 15 | 4. [Contributing](contributing/readme.md) 16 | 1. [GitHub](contributing/readme.md#github) 17 | 2. [Testing](contributing/readme.md#testing) -------------------------------------------------------------------------------- /docs/usage-package/readme.md: -------------------------------------------------------------------------------- 1 | # Using Parker as a Package 2 | 3 | Parker can be used as a package in scripts. It can be installed using npm like so: 4 | 5 | npm install --save parker 6 | 7 | Parker can then be used to run metrics on CSS on strings of stylesheet content: 8 | 9 | var Parker = require('parker'), 10 | metrics = require('./node_modules/parker/metrics/all'), 11 | parker = new Parker(metrics); 12 | 13 | console.log(parker.run('body {background: #fff;}')); 14 | 15 | /* 16 | { 'total-stylesheets': 1, 17 | 'total-stylesheet-size': 24, 18 | 'total-rules': 1, 19 | 'selectors-per-rule': 1, 20 | 'total-selectors': 1, 21 | 'identifiers-per-selector': 1, 22 | 'specificity-per-selector': 1, 23 | 'top-selector-specificity': 1, 24 | 'top-selector-specificity-selector': 'body', 25 | 'total-id-selectors': 0, 26 | 'total-identifiers': 1, 27 | 'total-declarations': 1, 28 | 'total-unique-colours': 1, 29 | 'unique-colours': [ '#FFFFFF' ], 30 | 'total-important-keywords': 0, 31 | 'total-media-queries': 0, 32 | 'media-queries': [] } 33 | */ 34 | 35 | Metrics have a bunch of properties that can help you select which ones you want to use: 36 | 37 | var metrics = require('./node_modules/parker/metrics/all'), 38 | numericMetrics = metrics.filter(metricIsNumeric), 39 | topSelectorSpecificityMetric = metrics.filter(metricIsTopSelectorSpecificity), 40 | selectorMetrics = metrics.filter(metricIsSelectorType); 41 | 42 | function metricIsNumeric(metric) { 43 | return metric.format == 'number'; 44 | } 45 | 46 | function metricIsTopSelectorSpecificity(metric) { 47 | return metric.id == 'top-selector-specificity'; 48 | } 49 | 50 | function metricIsSelectorType(metric) { 51 | return metric.type == 'selector'; 52 | } -------------------------------------------------------------------------------- /docs/usage/readme.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | Parker is a command-line tool. Switches and options are used to control how it works. By default Parker runs with recommended settings for beginners to aid discoverability. To make the most of Parker, it is recommended users read about the available options and choose which metrics are most useful to them. 3 | 4 | parker [arguments] [file...] 5 | 6 | Measuring local stylesheets: 7 | 8 | parker a.css b.css c.css 9 | 10 | Measuring all local stylesheets in a directory: 11 | 12 | parker css/ 13 | 14 | Measuring a remote stylesheet using curl: 15 | 16 | curl http://www.katiefenn.co.uk/css/shuttle.css | parker -s 17 | 18 | 19 | ## Options 20 | ### -f --format 21 | Set output format. 22 | 23 | Formats: 24 | 25 | - __human__: Default. Human-readable format. 26 | - __json__: JSON format for integration with scripts. 27 | - __csv__: Comma-separated-value format for output to spreadsheets. 28 | 29 | ### -h --help 30 | Shows help. 31 | 32 | ### -n --numeric 33 | Run numeric-format metrics only. Useful for making benchmarks. 34 | 35 | ### -s --stdin 36 | Input CSS using stdin. 37 | 38 | ### -v --version 39 | Show version number of Parker. 40 | -------------------------------------------------------------------------------- /lib/ArraySelectorSpecificity.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var CssSelector = require('../lib/CssSelector'); 6 | var _ = require('lodash'); 7 | 8 | module.exports = function (rawSelector) { 9 | var idIdentifiers = 0, 10 | classIdentifiers = 0, 11 | attributeIdentifiers = 0, 12 | pseudoClassIdentifiers = 0, 13 | typeIdentifiers = 0, 14 | pseudoElementIdentifiers = 0, 15 | selector = new CssSelector(rawSelector); 16 | 17 | _.each(selector.getIdentifiers(), function (identifier) { 18 | idIdentifiers += countIdIdentifiers(identifier); 19 | classIdentifiers += countClassIdentifiers(identifier); 20 | attributeIdentifiers += countAttributeIdentifiers(identifier); 21 | pseudoClassIdentifiers += countPseudoClassIdentifiers(identifier); 22 | typeIdentifiers += countTypeIdentifiers(identifier); 23 | pseudoElementIdentifiers = countPseudoElementIdentifiers(identifier); 24 | }); 25 | 26 | return [ 27 | idIdentifiers, 28 | classIdentifiers + attributeIdentifiers + pseudoClassIdentifiers, 29 | typeIdentifiers + pseudoElementIdentifiers 30 | ]; 31 | } 32 | 33 | var countIdIdentifiers = function (identifier) { 34 | var regex = /#/, 35 | matches = regex.exec(identifier); 36 | 37 | if (matches && !countAttributeIdentifiers(identifier)) { 38 | return matches.length; 39 | } 40 | 41 | return 0; 42 | }; 43 | 44 | var countClassIdentifiers = function (identifier) { 45 | var regex = /\./, 46 | matches = regex.exec(identifier); 47 | 48 | if (matches) { 49 | return matches.length; 50 | } 51 | 52 | return 0; 53 | }; 54 | 55 | var countAttributeIdentifiers = function (identifier) { 56 | var regex = /\[/, 57 | matches = regex.exec(identifier); 58 | 59 | if (matches) { 60 | return matches.length; 61 | } 62 | 63 | return 0; 64 | }; 65 | 66 | var countPseudoClassIdentifiers = function (identifier) { 67 | var regex = /^:[^:]/, 68 | matches = regex.exec(identifier); 69 | 70 | // :not pseudo-class identifier itself is ignored 71 | // only selectors inside it are counted 72 | if (identifier.match(/:not/)) { 73 | return 0; 74 | } 75 | 76 | if (matches) { 77 | return matches.length; 78 | } 79 | 80 | return 0; 81 | }; 82 | 83 | var countTypeIdentifiers = function (identifier) { 84 | var regex = /^[a-zA-Z_]/; 85 | 86 | if (regex.exec(identifier)) { 87 | return 1; 88 | } 89 | 90 | return 0; 91 | }; 92 | 93 | var countPseudoElementIdentifiers = function (identifier) { 94 | var regex = /::/, 95 | matches = regex.exec(identifier); 96 | 97 | if (matches) { 98 | return matches.length; 99 | } 100 | 101 | return 0; 102 | }; 103 | 104 | var stripNotIdentifier = function (identifier) { 105 | if (identifier.match(/:not/)) { 106 | return identifier.replace(/:not\(|\)/g, ''); 107 | } 108 | 109 | return identifier; 110 | }; 111 | -------------------------------------------------------------------------------- /lib/ClassicalSelectorSpecificity.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var CssSelector = require('../lib/CssSelector'); 6 | var _ = require('lodash'); 7 | 8 | module.exports = function (rawSelector) { 9 | var totalSpecificity = 0, 10 | selector = new CssSelector(rawSelector); 11 | 12 | _.each(selector.getIdentifiers(), function (identifier) { 13 | var idIdentifiers = countIdIdentifiers(identifier), 14 | classIdentifiers = countClassIdentifiers(identifier), 15 | attributeIdentifiers = countAttributeIdentifiers(identifier), 16 | pseudoClassIdentifiers = countPseudoClassIdentifiers(identifier), 17 | typeIdentifiers = countTypeIdentifiers(identifier), 18 | pseudoElementIdentifiers = countPseudoElementIdentifiers(identifier); 19 | 20 | totalSpecificity += Number( 21 | String(idIdentifiers) + 22 | String(classIdentifiers + attributeIdentifiers + pseudoClassIdentifiers) + 23 | String(typeIdentifiers + pseudoElementIdentifiers)); 24 | }); 25 | 26 | return totalSpecificity; 27 | } 28 | 29 | var countIdIdentifiers = function (identifier) { 30 | var regex = /#/, 31 | matches = regex.exec(identifier); 32 | 33 | if (matches && !countAttributeIdentifiers(identifier)) { 34 | return matches.length; 35 | } 36 | 37 | return 0; 38 | }; 39 | 40 | var countClassIdentifiers = function (identifier) { 41 | var regex = /\./, 42 | matches = regex.exec(identifier); 43 | 44 | if (matches) { 45 | return matches.length; 46 | } 47 | 48 | return 0; 49 | }; 50 | 51 | var countAttributeIdentifiers = function (identifier) { 52 | var regex = /\[/, 53 | matches = regex.exec(identifier); 54 | 55 | if (matches) { 56 | return matches.length; 57 | } 58 | 59 | return 0; 60 | }; 61 | 62 | var countPseudoClassIdentifiers = function (identifier) { 63 | var regex = /^:[^:]/, 64 | matches = regex.exec(identifier); 65 | 66 | // :not pseudo-class identifier itself is ignored 67 | // only selectors inside it are counted 68 | if (identifier.match(/:not/)) { 69 | return 0; 70 | } 71 | 72 | if (matches) { 73 | return matches.length; 74 | } 75 | 76 | return 0; 77 | }; 78 | 79 | var countTypeIdentifiers = function (identifier) { 80 | var regex = /^[a-zA-Z_]/; 81 | 82 | if (regex.exec(identifier)) { 83 | return 1; 84 | } 85 | 86 | return 0; 87 | }; 88 | 89 | var countPseudoElementIdentifiers = function (identifier) { 90 | var regex = /::/, 91 | matches = regex.exec(identifier); 92 | 93 | if (matches) { 94 | return matches.length; 95 | } 96 | 97 | return 0; 98 | }; 99 | 100 | var stripNotIdentifier = function (identifier) { 101 | if (identifier.match(/:not/)) { 102 | return identifier.replace(/:not\(|\)/g, ''); 103 | } 104 | 105 | return identifier; 106 | }; 107 | -------------------------------------------------------------------------------- /lib/CliController.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var util = require('util'), 6 | events = require('events'); 7 | 8 | function CliController() { 9 | events.EventEmitter.call(this); 10 | } 11 | 12 | util.inherits(CliController, events.EventEmitter); 13 | 14 | CliController.prototype.dispatch = function (argv) { 15 | if (argv.v || argv.version) { 16 | this.emit('showVersion'); 17 | } 18 | if (argv.h || argv.help) { 19 | this.emit('showHelp'); 20 | } 21 | if (argv.f || argv.format) { 22 | var format = argv.f || argv.format; 23 | this.emit('setFormat', format); 24 | } 25 | if (argv.n || argv.numeric) { 26 | this.emit('showNumericOnly'); 27 | } 28 | if (argv._ && argv._.length) { 29 | this.emit('runPaths', argv._); 30 | } 31 | else if (argv.s || argv.stdin) { 32 | this.emit('runStdin'); 33 | } 34 | else { 35 | // No data supplied - show help 36 | this.emit('showHelp'); 37 | } 38 | }; 39 | 40 | module.exports = CliController; -------------------------------------------------------------------------------- /lib/CliFormatter.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | 7 | function CliFormatter() { 8 | 9 | } 10 | 11 | CliFormatter.prototype.format = function (data, metrics) { 12 | var output = ''; 13 | _.each(metrics, function(metric) { 14 | output += metric.name + ": " + data[metric.id]; 15 | output += "\n"; 16 | }); 17 | 18 | return output; 19 | }; 20 | 21 | module.exports = CliFormatter; 22 | -------------------------------------------------------------------------------- /lib/CssDeclaration.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | function CssDeclaration(raw) { 6 | 7 | this.raw = raw; 8 | } 9 | 10 | CssDeclaration.prototype.getProperty = function () { 11 | if (this.raw.indexOf(':') === -1) { 12 | return ''; 13 | } 14 | 15 | return this.raw.split(':')[0].trim(); 16 | }; 17 | 18 | CssDeclaration.prototype.getValue = function () { 19 | if (this.raw.indexOf(':') === -1) { 20 | return ''; 21 | } 22 | 23 | return this.raw.split(':')[1].trim(); 24 | }; 25 | 26 | module.exports = CssDeclaration; -------------------------------------------------------------------------------- /lib/CssMediaQuery.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | function CssMediaQuery(raw) { 6 | this.raw = raw; 7 | } 8 | 9 | CssMediaQuery.prototype.getQueries = function () { 10 | var pattern = /@media\w*(.+?)\s?{/, 11 | queries = pattern.exec(this.raw)[1]; 12 | 13 | return queries.split(/ or |,/g).map(trimQuery); 14 | }; 15 | 16 | CssMediaQuery.prototype.getRules = function () { 17 | var rules = [], 18 | depth = 0, 19 | rule = ''; 20 | 21 | for (var index = 0; index < this.raw.length; index++) { 22 | if (depth > 0) { 23 | rule += this.raw.charAt(index); 24 | } 25 | if (this.raw.charAt(index) === '{') { 26 | depth ++; 27 | } 28 | else if (this.raw.charAt(index) == '}') { 29 | depth --; 30 | } 31 | 32 | if (depth === 1 && this.raw.charAt(index) == '}') { 33 | rules.push(rule.trim()); 34 | rule = ''; 35 | } 36 | } 37 | return rules; 38 | }; 39 | 40 | var trimQuery = function (query) { 41 | return query.trim(); 42 | }; 43 | 44 | module.exports = CssMediaQuery; -------------------------------------------------------------------------------- /lib/CssRule.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | 7 | function CssRule(raw) { 8 | this.raw = raw; 9 | } 10 | 11 | CssRule.prototype.getSelectors = function () { 12 | return getSelectors(getSelectorBlock(this.raw)); 13 | }; 14 | 15 | CssRule.prototype.getDeclarations = function () { 16 | return getDeclarations(getDeclarationBlock(this.raw)); 17 | }; 18 | 19 | var getSelectorBlock = function (rule) { 20 | var pattern = /([^{]+)\{/g, 21 | results = pattern.exec(rule); 22 | 23 | return results[1]; 24 | }; 25 | 26 | var getSelectors = function (selectorBlock) { 27 | var untrimmedSelectors = selectorBlock.split(','), 28 | trimmedSelectors = untrimmedSelectors.map(function (untrimmed) { 29 | return untrimmed.trim(); 30 | }); 31 | 32 | return _.compact(trimmedSelectors); 33 | }; 34 | 35 | var getDeclarationBlock = function (rule) { 36 | var pattern = /\{(.+)\}/g, 37 | results = pattern.exec(rule); 38 | 39 | if (_.isNull(results)) { 40 | return ''; 41 | } 42 | 43 | return results[1]; 44 | }; 45 | 46 | var getDeclarations = function (declarationBlock) { 47 | var untrimmedDeclarations = _.compact(declarationBlock.trim().split(';')), 48 | trimmedDeclarations = untrimmedDeclarations.map(function (untrimmed) { 49 | return untrimmed.trim(); 50 | }); 51 | 52 | return trimmedDeclarations; 53 | }; 54 | 55 | module.exports = CssRule; 56 | -------------------------------------------------------------------------------- /lib/CssSelector.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.1.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'), 6 | DELIMITERS = ['.', '#', '>', '[', ' ', ':', '*']; 7 | 8 | function CssSelector(raw) { 9 | this.raw = raw; 10 | this.identifiers = []; 11 | } 12 | 13 | CssSelector.prototype.getIdentifiers = function () { 14 | var identifier = '', 15 | bracketDepth = 0, 16 | parenDepth = 0; 17 | 18 | _.each(this.raw, function (character, index) { 19 | var insideBrackets = bracketDepth || parenDepth, 20 | isSecondColon = character == ':' && this.raw[index - 1] == ':'; 21 | 22 | if (!insideBrackets && isDelimiter(character) && !isSecondColon) { 23 | this.addIdentifier(identifier); 24 | identifier = ''; 25 | } 26 | 27 | switch(character) { 28 | case '(': parenDepth++; break; 29 | case ')': parenDepth--; break; 30 | case '[': bracketDepth++; break; 31 | case ']': bracketDepth--; break; 32 | } 33 | 34 | if (!_.contains([' ', '>'], character)) { 35 | identifier += character; 36 | } 37 | }, this); 38 | 39 | this.addIdentifier(identifier); 40 | return _.without(this.identifiers, ' ', '', '[]'); 41 | } 42 | 43 | CssSelector.prototype.addIdentifier = function (identifier) { 44 | this.identifiers.push(identifier); 45 | }; 46 | 47 | function isDelimiter(character) { 48 | return _.contains(DELIMITERS, character) 49 | } 50 | 51 | module.exports = CssSelector; 52 | -------------------------------------------------------------------------------- /lib/CssStylesheet.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | 7 | function CssStylesheet(raw) { 8 | this.raw = raw; 9 | } 10 | 11 | CssStylesheet.prototype.getRules = function () { 12 | this.children = this.children || getChildren(this.raw); 13 | 14 | return this.children.filter(function (child) { 15 | return isRule(child); 16 | }); 17 | }; 18 | 19 | CssStylesheet.prototype.getMalformedStatements = function () { 20 | this.children = this.children || getChildren(this.raw); 21 | 22 | return this.children.filter(function (child) { 23 | return isMalformedStatement(child); 24 | }); 25 | }; 26 | 27 | CssStylesheet.prototype.getMediaQueries = function () { 28 | this.children = this.children || getChildren(this.raw); 29 | return this.children.filter(function (child) { 30 | return isMediaQuery(child); 31 | }); 32 | }; 33 | 34 | var getChildren = function (raw) { 35 | var children = [], 36 | depth = 0, 37 | child = '', 38 | stylesheet = stripComments(stripFormatting(raw)); 39 | 40 | for (var index = 0; index < stylesheet.length; index++) { 41 | child += stylesheet.charAt(index); 42 | if (stylesheet.charAt(index) === '{') { 43 | depth ++; 44 | } 45 | else if (stylesheet.charAt(index) == '}') { 46 | depth --; 47 | } 48 | 49 | if (depth === 0 && stylesheet.charAt(index).match(/\}|;/g)) { 50 | children.push(child.trim()); 51 | child = ''; 52 | } 53 | } 54 | return children; 55 | }; 56 | 57 | var stripComments = function (string) { 58 | return string.replace(/\/\*.+?\*\//g, ''); 59 | }; 60 | 61 | var stripFormatting = function (string) { 62 | return stripNewlines(trimWhitespace(string)); 63 | }; 64 | 65 | var trimWhitespace = function (string) { 66 | return string.replace(/[ ]+/g, ' '); 67 | }; 68 | 69 | var stripNewlines = function (string) { 70 | return string.replace(/\n|\r|\r\n/g, ''); 71 | }; 72 | 73 | var isRule = function (string) { 74 | return !isMediaQuery(string) && hasRuleBlock(string) && hasSelectorBlock(string); 75 | } 76 | 77 | var isMalformedStatement = function (string) { 78 | return !isRule(string) && !isMediaQuery(string); 79 | } 80 | 81 | var hasRuleBlock = function (string) { 82 | return string.indexOf('{') !== -1 && string.indexOf('}') !== -1; 83 | } 84 | 85 | var hasSelectorBlock = function (string) { 86 | return string.match(/^[^\{]+/g) 87 | } 88 | 89 | var isMediaQuery = function (string) { 90 | return string.match(/^@media/g); 91 | } 92 | 93 | module.exports = CssStylesheet; 94 | -------------------------------------------------------------------------------- /lib/Formatters.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'), 6 | clc = require('cli-color'); 7 | 8 | 9 | // formats output as human-friendly text 10 | exports.human = function (metrics, results) { 11 | var logo = clc.red('PA') + clc.yellow('RK') + clc.green('ER') + '-JS' + "\n"; 12 | return logo + _.reduce(metrics, function (str, metric) { 13 | return str + metric.name + ': ' + results[metric.id] + '\n'; 14 | }, ''); 15 | }; 16 | 17 | // formats output as JSON 18 | exports.json = function (metrics, results) { 19 | var ids = _.map(metrics, function (metric) { 20 | return metric.id; 21 | }); 22 | var obj = _.reduce(ids, function (obj, id) { 23 | obj[id] = results[id]; 24 | return obj; 25 | }, {}); 26 | return JSON.stringify(obj, null, 4); 27 | }; 28 | 29 | exports.csv = function (metrics, results) { 30 | var lineItems = []; 31 | _.each(metrics, function (metric) { 32 | lineItems.push('"' + results[metric.id] + '"'); 33 | }); 34 | 35 | return lineItems.join(','); 36 | } 37 | -------------------------------------------------------------------------------- /lib/Info.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var pkg = require('../package.json'); 6 | 7 | module.exports = { 8 | version: function() { 9 | console.log(pkg.name + ' v' + pkg.version); 10 | }, 11 | help: function() { 12 | module.exports.version(); 13 | 14 | [ 15 | pkg.description, 16 | '', 17 | 'Usage:', 18 | 'parker [arguments] [file...] Run Parker on specified files', 19 | '', 20 | 'Example Local Usage:', 21 | 'parker styles.css', 22 | '', 23 | 'Example Stdin Usage:', 24 | 'curl http://www.katiefenn.co.uk/css/shuttle.css -s | parker -s', 25 | '', 26 | 'Arguments:', 27 | '', 28 | '-f Set output format (see list of formats)', 29 | '-h Shows help', 30 | '-n Show numeric results only', 31 | '-s Input CSS using stdin', 32 | '-v Show version number of Parker', 33 | '', 34 | 'Formats Usage:', 35 | 'parker -f "human"', 36 | '', 37 | 'Formats List:', 38 | 'human Human-readable, newline separated format (default)', 39 | 'json JSON', 40 | 'csv CSV', 41 | '', 42 | 'For more information, see ' + pkg.homepage 43 | ].forEach(function(str) { console.log(str); }); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /lib/Parker.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'), 6 | CssStylesheet = require('./CssStylesheet.js'), 7 | CssRule = require('./CssRule.js'), 8 | CssSelector = require('./CssSelector.js'), 9 | CssDeclaration = require('./CssDeclaration.js'), 10 | CssMediaQuery = require('./CssMediaQuery.js'); 11 | 12 | var VALID_METRIC_TYPE = /stylesheet|rule|selector|identifier|declaration|property|value|mediaquery/g; 13 | 14 | function Parker(metrics) { 15 | this.metrics = metrics; 16 | } 17 | 18 | Parker.prototype.run = function () { 19 | var args = arguments; 20 | 21 | if (arguments.length === 1 && _.isArray(arguments[0])) { 22 | args = arguments[0]; 23 | } 24 | 25 | args = _.filter(args, function (argument) {return _.isString(argument); }); 26 | var metrics = _.filter(this.metrics, function (metric) {return metric.type.match(VALID_METRIC_TYPE); }); 27 | 28 | if (args.length > 0) { 29 | return runMetrics(metrics, args); 30 | } 31 | 32 | throw {'message': 'No valid stylesheet data supplied', 'name': 'DataTypeException'}; 33 | }; 34 | 35 | var runMetrics = function (metrics, stylesheets) { 36 | var readings = []; 37 | 38 | _.each(stylesheets, function (rawStylesheet) { 39 | readings.push(runMetricsOnNode(metrics, rawStylesheet, 'stylesheet')); 40 | 41 | var stylesheet = new CssStylesheet(rawStylesheet), 42 | rules = stylesheet.getRules() || []; 43 | 44 | _.each(stylesheet.getMediaQueries(), function (rawMediaQuery) { 45 | var mediaQuery = new CssMediaQuery(rawMediaQuery); 46 | _.each(mediaQuery.getQueries(), function (query) { 47 | readings.push(runMetricsOnNode(metrics, query, 'mediaquery')); 48 | }); 49 | 50 | rules = rules.concat(mediaQuery.getRules()); 51 | }); 52 | 53 | _.each(rules, function (rawRule) { 54 | readings.push(runMetricsOnNode(metrics, rawRule, 'rule')); 55 | 56 | var rule = new CssRule(rawRule); 57 | _.each(rule.getSelectors(), function (rawSelector) { 58 | var selector = new CssSelector(rawSelector); 59 | readings.push(runMetricsOnNode(metrics, rawSelector, 'selector')); 60 | 61 | _.each(selector.getIdentifiers(), function (rawIdentifier) { 62 | readings.push(runMetricsOnNode(metrics, rawIdentifier, 'identifier')); 63 | }); 64 | }); 65 | 66 | _.each(rule.getDeclarations(), function (rawDeclaration) { 67 | var declaration = new CssDeclaration(rawDeclaration); 68 | 69 | readings.push(runMetricsOnNode(metrics, rawDeclaration, 'declaration')); 70 | readings.push(runMetricsOnNode(metrics, declaration.getProperty(), 'property')); 71 | readings.push(runMetricsOnNode(metrics, declaration.getValue(), 'value')); 72 | }); 73 | }); 74 | }); 75 | 76 | var data = readings.reduce(mergeArrayAttributes); 77 | data = aggregateData(data, metrics); 78 | 79 | return data; 80 | }; 81 | 82 | var runMetricsOnNode = function (metrics, node, type) { 83 | var data = {}; 84 | _.each(filterMetricsByType(metrics, type), function (metric) { 85 | var measurement = metric.measure(node); 86 | if (!_.isArray(measurement)) { 87 | measurement = [measurement]; 88 | } 89 | 90 | data[metric.id] = measurement; 91 | }); 92 | 93 | return data; 94 | }; 95 | 96 | var aggregateData = function (data, metrics) { 97 | _.each(metrics, function (metric) { 98 | if (!data[metric.id]) { 99 | data[metric.id] = []; 100 | } 101 | 102 | switch (metric.aggregate) { 103 | case 'sum': 104 | data[metric.id] = aggregateSum(data[metric.id]); 105 | break; 106 | case 'mean': 107 | data[metric.id] = aggregateMean(data[metric.id]); 108 | break; 109 | case 'max': 110 | data[metric.id] = aggregateMax(data[metric.id], metric.iterator); 111 | break; 112 | case 'list': 113 | data[metric.id] = aggregateList(data[metric.id], metric.filter); 114 | break; 115 | case 'length': 116 | data[metric.id] = aggregateLength(data[metric.id], metric.filter); 117 | break; 118 | case 'reduce': 119 | data[metric.id] = aggregateReduce(data[metric.id], metric.reduce, metric.initial); 120 | break; 121 | } 122 | }); 123 | 124 | return data; 125 | }; 126 | 127 | var aggregateSum = function (data) { 128 | return sum(data); 129 | }; 130 | 131 | var aggregateMean = function (data) { 132 | if (data.length === 0) { 133 | return 0; 134 | } 135 | return mean(data); 136 | }; 137 | 138 | var aggregateMax = function (data, iterator) { 139 | if (data.length === 0) { 140 | return 0; 141 | } 142 | if (_.isUndefined(iterator)) { 143 | return _.max(data); 144 | } 145 | 146 | return _.max(data, iterator); 147 | }; 148 | 149 | var aggregateList = function (data, filter) { 150 | if (!_.isUndefined(filter)) { 151 | return _.compact(data.filter(filter)); 152 | } 153 | 154 | return _.compact(data); 155 | }; 156 | 157 | var aggregateLength = function (data, filter) { 158 | if (!_.isUndefined(filter)) { 159 | return data.filter(filter).length; 160 | } 161 | 162 | return _.compact(data).length; 163 | }; 164 | 165 | var aggregateReduce = function (data, reduce, initial) { 166 | return data.reduce(reduce, initial); 167 | }; 168 | 169 | var mergeArrayAttributes = function (target, source) { 170 | _.each(source, function (attribute, attributeName) { 171 | if (!_.has(target, attributeName)) { 172 | target[attributeName] = []; 173 | } 174 | 175 | if (!_.isUndefined(attribute)) { 176 | if (_.isString(attribute)) { 177 | attribute = [attribute]; 178 | } 179 | 180 | target[attributeName] = target[attributeName].concat(attribute); 181 | } 182 | }); 183 | 184 | return target; 185 | }; 186 | 187 | var sum = function (values) { 188 | if (values.length === 0) { 189 | return 0; 190 | } 191 | return values.reduce(function (previous, current) {return previous + current; }); 192 | }; 193 | 194 | var mean = function (values) { 195 | var valuesSum = sum(values); 196 | 197 | return valuesSum / values.length; 198 | }; 199 | 200 | var filterMetricsByType = function (metrics, type) { 201 | if (type) { 202 | return metrics.filter(function (metric) {return metric.type === type; }); 203 | } 204 | 205 | return metrics; 206 | }; 207 | 208 | module.exports = Parker; 209 | -------------------------------------------------------------------------------- /lib/PreciseSelectorSpecificity.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var CssSelector = require('../lib/CssSelector'); 6 | var _ = require('lodash'); 7 | 8 | var CLASS_A = 1; 9 | var CLASS_B = 256; 10 | var CLASS_C = 65536; 11 | 12 | module.exports = function (rawSelector) { 13 | var totalSpecificity = 0, 14 | selector = new CssSelector(rawSelector); 15 | 16 | _.each(selector.getIdentifiers(), function (identifier) { 17 | var idIdentifiers = countIdIdentifiers(identifier), 18 | classIdentifiers = countClassIdentifiers(identifier), 19 | attributeIdentifiers = countAttributeIdentifiers(identifier), 20 | pseudoClassIdentifiers = countPseudoClassIdentifiers(identifier), 21 | typeIdentifiers = countTypeIdentifiers(identifier), 22 | pseudoElementIdentifiers = countPseudoElementIdentifiers(identifier); 23 | 24 | totalSpecificity += Number( 25 | (idIdentifiers * CLASS_C) + 26 | ((classIdentifiers + attributeIdentifiers + pseudoClassIdentifiers) * CLASS_B) + 27 | ((typeIdentifiers + pseudoElementIdentifiers) * CLASS_A)); 28 | }); 29 | 30 | return totalSpecificity; 31 | } 32 | 33 | var countIdIdentifiers = function (identifier) { 34 | var regex = /#/, 35 | matches = regex.exec(identifier); 36 | 37 | if (matches && !countAttributeIdentifiers(identifier)) { 38 | return matches.length; 39 | } 40 | 41 | return 0; 42 | }; 43 | 44 | var countClassIdentifiers = function (identifier) { 45 | var regex = /\./, 46 | matches = regex.exec(identifier); 47 | 48 | if (matches) { 49 | return matches.length; 50 | } 51 | 52 | return 0; 53 | }; 54 | 55 | var countAttributeIdentifiers = function (identifier) { 56 | var regex = /\[/, 57 | matches = regex.exec(identifier); 58 | 59 | if (matches) { 60 | return matches.length; 61 | } 62 | 63 | return 0; 64 | }; 65 | 66 | var countPseudoClassIdentifiers = function (identifier) { 67 | var regex = /^:[^:]/, 68 | matches = regex.exec(identifier); 69 | 70 | // :not pseudo-class identifier itself is ignored 71 | // only selectors inside it are counted 72 | if (identifier.match(/:not/)) { 73 | return 0; 74 | } 75 | 76 | if (matches) { 77 | return matches.length; 78 | } 79 | 80 | return 0; 81 | }; 82 | 83 | var countTypeIdentifiers = function (identifier) { 84 | var regex = /^[a-zA-Z_]/; 85 | 86 | if (regex.exec(identifier)) { 87 | return 1; 88 | } 89 | 90 | return 0; 91 | }; 92 | 93 | var countPseudoElementIdentifiers = function (identifier) { 94 | var regex = /::/, 95 | matches = regex.exec(identifier); 96 | 97 | if (matches) { 98 | return matches.length; 99 | } 100 | 101 | return 0; 102 | }; 103 | 104 | var stripNotIdentifier = function (identifier) { 105 | if (identifier.match(/:not/)) { 106 | return identifier.replace(/:not\(|\)/g, ''); 107 | } 108 | 109 | return identifier; 110 | }; 111 | -------------------------------------------------------------------------------- /metrics/All.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | module.exports = [ 6 | // Stylesheet Totals 7 | require('./TotalStylesheets.js'), 8 | require('./TotalStylesheetSize.js'), 9 | 10 | // Stylesheet Element Totals 11 | require('./TotalRules.js'), 12 | require('./TotalSelectors.js'), 13 | require('./TotalIdentifiers.js'), 14 | require('./TotalDeclarations.js'), 15 | 16 | // Stylesheet Element Averages 17 | require('./SelectorsPerRule.js'), 18 | require('./IdentifiersPerSelector.js'), 19 | require('./DeclarationsPerRule.js'), 20 | 21 | // Specificity 22 | require('./SpecificityPerSelector.js'), 23 | require('./PreciseSpecificityPerSelector.js'), 24 | require('./TopSelectorSpecificity.js'), 25 | require('./PreciseTopSelectorSpecificity.js'), 26 | require('./StringTopSelectorSpecificity.js'), 27 | require('./TopSelectorSpecificitySelector.js'), 28 | require('./TotalIdSelectors.js'), 29 | 30 | // Colour 31 | require('./TotalUniqueColours.js'), 32 | require('./UniqueColours.js'), 33 | 34 | // Important Keywords 35 | require('./TotalImportantKeywords.js'), 36 | 37 | // Media Queries 38 | require('./TotalMediaQueries.js'), 39 | require('./MediaQueries.js'), 40 | 41 | // Z-Index 42 | require('./ZIndexes.js') 43 | ]; 44 | -------------------------------------------------------------------------------- /metrics/DeclarationsPerRule.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var CssRule = require('../lib/CssRule'); 4 | 5 | var _ = require('lodash'); 6 | module.exports = { 7 | id: 'declarations-per-rule', 8 | name: 'Declarations Per Rule', 9 | type: 'rule', 10 | aggregate: 'mean', 11 | format: 'number', 12 | measure: function (raw) { 13 | var rule = new CssRule(raw); 14 | return rule.getDeclarations().length; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /metrics/IdentifiersPerSelector.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | 7 | module.exports = { 8 | id: 'identifiers-per-selector', 9 | name: 'Identifiers Per Selector', 10 | type: 'selector', 11 | aggregate: 'mean', 12 | format: 'number', 13 | measure: function (selector) { 14 | var identifiers = getIdentifiers(selector); 15 | 16 | if (identifiers.length === 1 && identifiers[0] === '') { 17 | return 0; 18 | } 19 | 20 | return identifiers.length; 21 | } 22 | }; 23 | 24 | var getIdentifiers = function (selector) { 25 | var identifiers = [], 26 | segments = selector.split(/\s+[\s\+>]\s?|~^=/g); 27 | 28 | _.each(segments, function (segment) { 29 | identifiers = identifiers.concat(segment.match(/[#\.:]?[\w\-\*]+|\[[\w=\-~'"\|]+\]|:{2}[\w-]+/g) || []); 30 | }); 31 | 32 | return identifiers; 33 | }; -------------------------------------------------------------------------------- /metrics/MediaQueries.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | 7 | module.exports = { 8 | id: 'media-queries', 9 | name: 'Media Queries', 10 | type: 'mediaquery', 11 | aggregate: 'list', 12 | format: 'list', 13 | measure: function (query) { 14 | return query; 15 | }, 16 | filter: function (value, index, self) { 17 | return self.indexOf(value) === index; 18 | } 19 | }; -------------------------------------------------------------------------------- /metrics/PreciseSpecificityPerSelector.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | var CssSelector = require('../lib/CssSelector'); 7 | var getSpecificity = require('../lib/PreciseSelectorSpecificity'); 8 | 9 | module.exports = { 10 | id: 'precise-specificity-per-selector', 11 | name: 'Specificity Per Selector (Precise)', 12 | type: 'selector', 13 | aggregate: 'mean', 14 | format: 'number', 15 | measure: getSpecificity 16 | }; 17 | -------------------------------------------------------------------------------- /metrics/PreciseTopSelectorSpecificity.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | var CssSelector = require('../lib/CssSelector'); 7 | var getSpecificity = require('../lib/PreciseSelectorSpecificity'); 8 | 9 | module.exports = { 10 | id: 'precise-top-selector-specificity', 11 | name: 'Top Selector Specificity (Precise)', 12 | type: 'selector', 13 | aggregate: 'max', 14 | format: 'number', 15 | measure: getSpecificity 16 | }; 17 | -------------------------------------------------------------------------------- /metrics/SelectorsPerRule.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | module.exports = { 6 | id: 'selectors-per-rule', 7 | name: 'Selectors Per Rule', 8 | type: 'rule', 9 | aggregate: 'mean', 10 | format: 'number', 11 | measure: function (rule) { 12 | return getSelectors(getSelectorBlock(rule)).length; 13 | } 14 | }; 15 | 16 | var getSelectorBlock = function (rule) { 17 | var pattern = /([^{]+)\{/g, 18 | results = pattern.exec(rule); 19 | 20 | return results[1]; 21 | }; 22 | 23 | var getSelectors = function (selectorBlock) { 24 | var untrimmedSelectors = selectorBlock.split(','), 25 | trimmedSelectors = untrimmedSelectors.map(function (untrimmed) { 26 | return untrimmed.trim(); 27 | }); 28 | 29 | return trimmedSelectors; 30 | }; -------------------------------------------------------------------------------- /metrics/SpecificityPerSelector.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | var CssSelector = require('../lib/CssSelector'); 7 | var getSpecificity = require('../lib/ClassicalSelectorSpecificity'); 8 | 9 | module.exports = { 10 | id: 'specificity-per-selector', 11 | name: 'Specificity Per Selector (Classical)', 12 | type: 'selector', 13 | aggregate: 'mean', 14 | format: 'number', 15 | measure: getSpecificity 16 | }; 17 | -------------------------------------------------------------------------------- /metrics/StringTopSelectorSpecificity.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | var CssSelector = require('../lib/CssSelector'); 7 | var getSpecificity = require('../lib/ArraySelectorSpecificity'); 8 | 9 | module.exports = { 10 | id: 'string-top-selector-specificity', 11 | name: 'Top Selector Specificity (String)', 12 | type: 'selector', 13 | aggregate: 'reduce', 14 | format: 'string', 15 | initial: '0,0,0', 16 | measure: function(selector) { 17 | return selector; 18 | }, 19 | reduce: function(previous, current) { 20 | var previousSpecificity = fromString(previous); 21 | var currentSpecificity = getSpecificity(current); 22 | 23 | return toString(mostSpecific(previousSpecificity, currentSpecificity)); 24 | } 25 | }; 26 | 27 | function toString(arr) { 28 | return arr.map(function(item) { 29 | return item.toString(); 30 | }).join(','); 31 | } 32 | 33 | function fromString(str) { 34 | return str.split(',').map(function(item) { 35 | return parseInt(item); 36 | }); 37 | } 38 | 39 | function mostSpecific(arr1, arr2) { 40 | var arr1Specificity = (arr1[0] * 65536) + (arr1[1] * 256) + (arr1[2] * 1); 41 | var arr2Specificity = (arr2[0] * 65536) + (arr2[1] * 256) + (arr2[2] * 1); 42 | 43 | if (arr1Specificity > arr2Specificity) { 44 | return arr1; 45 | } 46 | 47 | return arr2; 48 | } 49 | -------------------------------------------------------------------------------- /metrics/TopSelectorSpecificity.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | var CssSelector = require('../lib/CssSelector'); 7 | 8 | module.exports = { 9 | id: 'top-selector-specificity', 10 | name: 'Top Selector Specificity (Classical)', 11 | type: 'selector', 12 | aggregate: 'max', 13 | format: 'number', 14 | measure: function (rawSelector) { 15 | var selector = new CssSelector(rawSelector), 16 | identifiers = selector.getIdentifiers(), 17 | specificity = 0; 18 | 19 | _.each(identifiers, function (identifier) { 20 | identifier = stripNotIdentifier(identifier); 21 | 22 | var idIdentifiers = countIdIdentifiers(identifier), 23 | classIdentifiers = countClassIdentifiers(identifier), 24 | attributeIdentifiers = countAttributeIdentifiers(identifier), 25 | pseudoClassIdentifiers = countPseudoClassIdentifiers(identifier), 26 | typeIdentifiers = countTypeIdentifiers(identifier), 27 | pseudoElementIdentifiers = countPseudoElementIdentifiers(identifier); 28 | 29 | specificity += getSpecificity(idIdentifiers, classIdentifiers, attributeIdentifiers, pseudoClassIdentifiers, typeIdentifiers, pseudoElementIdentifiers); 30 | 31 | }, this); 32 | 33 | return specificity; 34 | } 35 | }; 36 | 37 | var getSpecificity = function (idIdentifiers, classIdentifiers, attributeIdentifiers, pseudoClassIdentifiers, typeIdentifiers, pseudoElementIdentifiers) { 38 | return Number( 39 | String(idIdentifiers) + 40 | String(classIdentifiers + attributeIdentifiers + pseudoClassIdentifiers) + 41 | String(typeIdentifiers + pseudoElementIdentifiers) 42 | ); 43 | }; 44 | 45 | var countIdIdentifiers = function (identifier) { 46 | var regex = /#/, 47 | matches = regex.exec(identifier); 48 | 49 | if (matches && !countAttributeIdentifiers(identifier)) { 50 | return matches.length; 51 | } 52 | 53 | return 0; 54 | }; 55 | 56 | var countClassIdentifiers = function (identifier) { 57 | var regex = /\./, 58 | matches = regex.exec(identifier); 59 | 60 | if (matches) { 61 | return matches.length; 62 | } 63 | 64 | return 0; 65 | }; 66 | 67 | var countAttributeIdentifiers = function (identifier) { 68 | var regex = /\[/, 69 | matches = regex.exec(identifier); 70 | 71 | if (matches) { 72 | return matches.length; 73 | } 74 | 75 | return 0; 76 | }; 77 | 78 | var countPseudoClassIdentifiers = function (identifier) { 79 | var regex = /^:[^:]/, 80 | matches = regex.exec(identifier); 81 | 82 | // :not pseudo-class identifier itself is ignored 83 | // only selectors inside it are counted 84 | if (identifier.match(/:not/)) { 85 | return 0; 86 | } 87 | 88 | if (matches) { 89 | return matches.length; 90 | } 91 | 92 | return 0; 93 | }; 94 | 95 | var countTypeIdentifiers = function (identifier) { 96 | var regex = /^[a-zA-Z_]/; 97 | 98 | if (regex.exec(identifier)) { 99 | return 1; 100 | } 101 | 102 | return 0; 103 | }; 104 | 105 | var countPseudoElementIdentifiers = function (identifier) { 106 | var regex = /::/, 107 | matches = regex.exec(identifier); 108 | 109 | if (matches) { 110 | return matches.length; 111 | } 112 | 113 | return 0; 114 | }; 115 | 116 | var stripNotIdentifier = function (identifier) { 117 | if (identifier.match(/:not/)) { 118 | return identifier.replace(/:not\(|\)/g, ''); 119 | } 120 | 121 | return identifier; 122 | }; 123 | -------------------------------------------------------------------------------- /metrics/TopSelectorSpecificitySelector.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | var CssSelector = require('../lib/CssSelector'); 7 | var getSpecificity = require('../lib/PreciseSelectorSpecificity'); 8 | 9 | module.exports = { 10 | id: 'top-selector-specificity-selector', 11 | name: 'Top Selector Specificity Selector', 12 | type: 'selector', 13 | aggregate: 'max', 14 | format: 'string', 15 | measure: function(selector) { 16 | return selector; 17 | }, 18 | iterator: getSpecificity 19 | }; 20 | 21 | var getSpecificity = function (idIdentifiers, classIdentifiers, attributeIdentifiers, pseudoClassIdentifiers, typeIdentifiers, pseudoElementIdentifiers) { 22 | return Number( 23 | String(idIdentifiers) + 24 | String(classIdentifiers + attributeIdentifiers + pseudoClassIdentifiers) + 25 | String(typeIdentifiers + pseudoElementIdentifiers) 26 | ); 27 | }; 28 | 29 | var countIdIdentifiers = function (identifier) { 30 | var regex = /#/, 31 | matches = regex.exec(identifier); 32 | 33 | if (matches && !countAttributeIdentifiers(identifier)) { 34 | return matches.length; 35 | } 36 | 37 | return 0; 38 | }; 39 | 40 | var countClassIdentifiers = function (identifier) { 41 | var regex = /\./, 42 | matches = regex.exec(identifier); 43 | 44 | if (matches) { 45 | return matches.length; 46 | } 47 | 48 | return 0; 49 | }; 50 | 51 | var countAttributeIdentifiers = function (identifier) { 52 | var regex = /\[/, 53 | matches = regex.exec(identifier); 54 | 55 | if (matches) { 56 | return matches.length; 57 | } 58 | 59 | return 0; 60 | }; 61 | 62 | var countPseudoClassIdentifiers = function (identifier) { 63 | var regex = /^:[^:]/, 64 | matches = regex.exec(identifier); 65 | 66 | // :not pseudo-class identifier itself is ignored 67 | // only selectors inside it are counted 68 | if (identifier.match(/:not/)) { 69 | return 0; 70 | } 71 | 72 | if (matches) { 73 | return matches.length; 74 | } 75 | 76 | return 0; 77 | }; 78 | 79 | var countTypeIdentifiers = function (identifier) { 80 | var regex = /^[a-zA-Z_]/; 81 | 82 | if (regex.exec(identifier)) { 83 | return 1; 84 | } 85 | 86 | return 0; 87 | }; 88 | 89 | var countPseudoElementIdentifiers = function (identifier) { 90 | var regex = /::/, 91 | matches = regex.exec(identifier); 92 | 93 | if (matches) { 94 | return matches.length; 95 | } 96 | 97 | return 0; 98 | }; 99 | 100 | var stripNotIdentifier = function (identifier) { 101 | if (identifier.match(/:not/)) { 102 | return identifier.replace(/:not\(|\)/g, ''); 103 | } 104 | 105 | return identifier; 106 | }; 107 | -------------------------------------------------------------------------------- /metrics/TotalDeclarations.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | module.exports = { 6 | id: 'total-declarations', 7 | name: 'Total Declarations', 8 | type: 'declaration', 9 | aggregate: 'sum', 10 | format: 'number', 11 | measure: function (declaration) { 12 | return 1; 13 | } 14 | }; -------------------------------------------------------------------------------- /metrics/TotalIdSelectors.js: -------------------------------------------------------------------------------- 1 | 2 | /*! Parker v0.0.0 - MIT license */ 3 | 4 | 'use strict'; 5 | 6 | var _ = require('lodash'); 7 | 8 | module.exports = { 9 | id: 'total-id-selectors', 10 | name: 'Total Id Selectors', 11 | type: 'selector', 12 | aggregate: 'sum', 13 | format: 'number', 14 | measure: function (selector) { 15 | var ids = 0; 16 | var inBrackets = false; 17 | 18 | _.forOwn(selector, function (char) { 19 | if (char === '[') { 20 | inBrackets = true; 21 | } else if (char === ']') { 22 | inBrackets = false; 23 | } else if (char === '#' && !inBrackets) { 24 | ids++; 25 | } 26 | }); 27 | 28 | return ids; 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /metrics/TotalIdentifiers.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | 7 | module.exports = { 8 | id: 'total-identifiers', 9 | name: 'Total Identifiers', 10 | type: 'identifier', 11 | aggregate: 'sum', 12 | format: 'number', 13 | measure: function (identifier) { 14 | return 1; 15 | } 16 | }; -------------------------------------------------------------------------------- /metrics/TotalImportantKeywords.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | 7 | module.exports = { 8 | id: 'total-important-keywords', 9 | name: 'Total Important Keywords', 10 | type: 'value', 11 | aggregate: 'sum', 12 | format: 'number', 13 | measure: function (value) { 14 | if (value.match(/!important/g)) 15 | return 1; 16 | return 0; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /metrics/TotalMediaQueries.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | 7 | module.exports = { 8 | id: 'total-media-queries', 9 | name: 'Total Media Queries', 10 | type: 'mediaquery', 11 | aggregate: 'length', 12 | format: 'number', 13 | measure: function (query) { 14 | return query; 15 | }, 16 | filter: function (value, index, self) { 17 | return self.indexOf(value) === index; 18 | } 19 | }; -------------------------------------------------------------------------------- /metrics/TotalRules.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | module.exports = { 6 | id: 'total-rules', 7 | name: 'Total Rules', 8 | type: 'rule', 9 | aggregate: 'sum', 10 | format: 'number', 11 | measure: function (rule) { 12 | return 1; 13 | } 14 | }; -------------------------------------------------------------------------------- /metrics/TotalSelectors.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | 7 | module.exports = { 8 | id: 'total-selectors', 9 | name: 'Total Selectors', 10 | type: 'selector', 11 | aggregate: 'sum', 12 | format: 'number', 13 | measure: function (selector) { 14 | return 1; 15 | } 16 | }; -------------------------------------------------------------------------------- /metrics/TotalStylesheetSize.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | module.exports = { 6 | id: 'total-stylesheet-size', 7 | name: 'Total Stylesheet Size', 8 | type: 'stylesheet', 9 | aggregate: 'sum', 10 | format: 'number', 11 | measure: function (stylesheet) { 12 | return byteCount(stylesheet); 13 | } 14 | }; 15 | 16 | function byteCount(s) { 17 | return encodeURI(s).split(/%..|./).length - 1; 18 | } -------------------------------------------------------------------------------- /metrics/TotalStylesheets.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | module.exports = { 6 | id: 'total-stylesheets', 7 | name: 'Total Stylesheets', 8 | type: 'stylesheet', 9 | aggregate: 'sum', 10 | format: 'number', 11 | measure: function (stylesheet) { 12 | return 1; 13 | } 14 | }; -------------------------------------------------------------------------------- /metrics/TotalUniqueColours.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | 7 | module.exports = { 8 | id: 'total-unique-colours', 9 | name: 'Total Unique Colors', 10 | type: 'value', 11 | aggregate: 'length', 12 | format: 'number', 13 | measure: function (value) { 14 | return getColourHexes(value).map(function (colourHex) { 15 | return getLongHashForm(colourHex).toUpperCase(); 16 | }); 17 | }, 18 | filter: function (value, index, self) { 19 | return self.indexOf(value) === index; 20 | } 21 | }; 22 | 23 | var getColourHexes = function (value) { 24 | var colourHexes = value.match(/#[0-9a-fA-F]{3,6}/g); 25 | 26 | if (_.isNull(colourHexes)) { 27 | colourHexes = []; 28 | } 29 | 30 | return colourHexes; 31 | }; 32 | 33 | var getLongHashForm = function (string) { 34 | if (string.length === 4) { 35 | var r = string.substring(1, 2), 36 | g = string.substring(2, 3), 37 | b = string.substring(3); 38 | return '#' + r + r + g + g + b + b; 39 | } 40 | 41 | return string; 42 | }; -------------------------------------------------------------------------------- /metrics/UniqueColours.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var _ = require('lodash'); 6 | 7 | module.exports = { 8 | id: 'unique-colours', 9 | name: 'Unique Colors', 10 | type: 'value', 11 | aggregate: 'list', 12 | format: 'list', 13 | measure: function (value) { 14 | return getColourHexes(value).map(function (colourHex) { 15 | return getLongHashForm(colourHex).toUpperCase(); 16 | }); 17 | }, 18 | filter: function (value, index, self) { 19 | return self.indexOf(value) === index; 20 | } 21 | }; 22 | 23 | var getColourHexes = function (value) { 24 | var colourHexes = value.match(/#[0-9a-fA-F]{3,6}/g); 25 | 26 | if (_.isNull(colourHexes)) { 27 | colourHexes = []; 28 | } 29 | 30 | return colourHexes; 31 | }; 32 | 33 | var getLongHashForm = function (string) { 34 | if (string.length === 4) { 35 | var r = string.substring(1, 2), 36 | g = string.substring(2, 3), 37 | b = string.substring(3); 38 | return '#' + r + r + g + g + b + b; 39 | } 40 | 41 | return string; 42 | }; -------------------------------------------------------------------------------- /metrics/ZIndexes.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | 'use strict'; 4 | 5 | var CssDeclaration = require('../lib/CssDeclaration'); 6 | 7 | module.exports = { 8 | id: 'z-indexes', 9 | name: 'Z-Indexes', 10 | type: 'declaration', 11 | aggregate: 'list', 12 | format: 'list', 13 | measure: function (raw) { 14 | var declaration = new CssDeclaration(raw); 15 | 16 | if (declaration.getProperty() == "z-index") { 17 | return declaration.getValue(); 18 | } 19 | }, 20 | filter: function (value, index, self) { 21 | return value != undefined && self.indexOf(value) === index; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parker", 3 | "description": "Stylesheet analysis tool for CSS", 4 | "keywords": [ 5 | "css", 6 | "stylesheet", 7 | "analysis" 8 | ], 9 | "version": "1.0.0-alpha.0", 10 | "main": "parker.js", 11 | "dependencies": { 12 | "async": "~0.2.10", 13 | "cli-color": "*", 14 | "graceful-fs": "~3.0.2", 15 | "lodash": "^3.2.0", 16 | "minimist": "0.0.7" 17 | }, 18 | "devDependencies": { 19 | "chai": "*", 20 | "mocha": "*", 21 | "sinon": "*", 22 | "sinon-chai": "*" 23 | }, 24 | "scripts": { 25 | "test": "mocha --no-colors --reporter spec" 26 | }, 27 | "bin": { 28 | "parker": "./parker.js" 29 | }, 30 | "homepage": "https://github.com/katiefenn/parker", 31 | "bugs": "https://github.com/katiefenn/parker/issues", 32 | "author": "Katie Fenn", 33 | "repository": "https://github.com/katiefenn/parker", 34 | "preferGlobal": true, 35 | "license": "MIT" 36 | } 37 | -------------------------------------------------------------------------------- /parker.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /*! csstool v0.0.0 - MIT license */ 4 | 5 | 'use strict'; 6 | 7 | /** 8 | * Module dependencies 9 | */ 10 | 11 | var _ = require('lodash'), 12 | Parker = require('./lib/Parker'), 13 | CliController = require('./lib/CliController'), 14 | metrics = require('./metrics/All'), 15 | formatters = require('./lib/Formatters'), 16 | argv = require('minimist')(process.argv.slice(2)), 17 | fs = require('graceful-fs'), 18 | async = require('async'), 19 | path = require('path'), 20 | info = require('./lib/Info'); 21 | 22 | var cliController = new CliController(); 23 | 24 | cliController.on('runPaths', function (filePaths) { 25 | var stylesheets = []; 26 | async.each(filePaths, function (filePath, onAllLoad) { 27 | var onFileLoad = function (err, data) { 28 | stylesheets.push(data); 29 | }; 30 | read(filePath, onFileLoad, onAllLoad); 31 | }, function (err) { 32 | runReport(stylesheets, metrics); 33 | }); 34 | }); 35 | 36 | cliController.on('runStdin', function () { 37 | process.stdin.resume(); 38 | process.stdin.setEncoding('utf8'); 39 | var stdinData = ''; 40 | 41 | process.stdin.on('data', function(chunk) { 42 | stdinData += chunk; 43 | }); 44 | 45 | process.stdin.on('end', function() { 46 | runReport(stdinData, metrics); 47 | }); 48 | }); 49 | 50 | cliController.on('showVersion', function () { 51 | info.version(); 52 | process.exit(); 53 | }); 54 | 55 | cliController.on('showHelp', function () { 56 | info.help(); 57 | process.exit(); 58 | }); 59 | 60 | cliController.on('setFormat', function (format) { 61 | formatter = formatters[format]; 62 | 63 | if (!formatter) { 64 | console.error('Unknown output format: %s', argv.format); 65 | console.error(' available: ' + Object.keys(formatters).join(' ')); 66 | process.exit(1); 67 | } 68 | }); 69 | 70 | cliController.on('showNumericOnly', function () { 71 | metrics = _.filter(metrics, function (metric) { 72 | return metric.format == 'number'; 73 | }); 74 | }); 75 | 76 | var read = function (filePath, onFileLoad, onAllLoad) { 77 | if (fs.lstatSync(filePath).isDirectory()) { 78 | readDirectory(filePath, onFileLoad, onAllLoad); 79 | } 80 | else if (fileIsStylesheet(filePath)) { 81 | readFile(filePath, function (err, data) { 82 | onFileLoad(err, data); 83 | onAllLoad(); 84 | }); 85 | } else { 86 | onAllLoad(); 87 | } 88 | } 89 | 90 | var readDirectory = function (directoryPath, onFileLoad, onAllLoad) { 91 | fs.readdir(directoryPath, function (err, files) { 92 | async.each(files, function (file, fileDone) { 93 | read(path.join(directoryPath, file), onFileLoad, fileDone); 94 | }, onAllLoad); 95 | }); 96 | }; 97 | 98 | var readFile = function (filePath, onLoad) { 99 | fs.readFile(filePath, {encoding: 'utf8'}, function (err, fileData) { 100 | onLoad(err, fileData); 101 | }); 102 | }; 103 | 104 | var fileIsStylesheet = function (filePath) { 105 | return filePath.indexOf('.css') !== -1; 106 | }; 107 | 108 | var runReport = function (stylesheets, metrics) { 109 | var results = parker.run(stylesheets); 110 | console.log(formatter(metrics, results)); 111 | }; 112 | 113 | if (module.parent) { 114 | module.exports = Parker; 115 | } else { 116 | var parker = new Parker(metrics), 117 | formatter = formatters['human']; 118 | cliController.dispatch(argv); 119 | } 120 | -------------------------------------------------------------------------------- /test/CliController.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | var chai = require('chai'), 4 | expect = require('chai').expect, 5 | CliController = require('../lib/CliController.js'), 6 | sinon = require('sinon'), 7 | sinonChai = require('sinon-chai'); 8 | 9 | chai.use(sinonChai); 10 | 11 | describe('The CLI Controller', function() { 12 | it('responds to a -v switch by dispatching a version event', function () { 13 | var callback = sinon.spy(), 14 | cliController = new CliController(); 15 | 16 | cliController.on('showVersion', callback); 17 | cliController.dispatch({'v': true}); 18 | expect(callback).to.have.been.called; 19 | }); 20 | 21 | it('responds to a --version switch by dispatching a version event', function () { 22 | var callback = sinon.spy(), 23 | cliController = new CliController(); 24 | 25 | cliController.on('showVersion', callback); 26 | cliController.dispatch({'version': true}); 27 | expect(callback).to.have.been.called; 28 | }); 29 | 30 | it('responds to a -h switch by dispatching a version event', function () { 31 | var callback = sinon.spy(), 32 | cliController = new CliController(); 33 | 34 | cliController.on('showHelp', callback); 35 | cliController.dispatch({'h': true}); 36 | expect(callback).to.have.been.called; 37 | }); 38 | 39 | it('responds to a --help switch by dispatching a version event', function () { 40 | var callback = sinon.spy(), 41 | cliController = new CliController(); 42 | 43 | cliController.on('showHelp', callback); 44 | cliController.dispatch({'help': true}); 45 | expect(callback).to.have.been.called; 46 | }); 47 | 48 | it('responds to a -h switch by dispatching a help event', function () { 49 | var callback = sinon.spy(), 50 | cliController = new CliController(); 51 | 52 | cliController.on('showHelp', callback); 53 | cliController.dispatch({'h': true}); 54 | expect(callback).to.have.been.called; 55 | }); 56 | 57 | it('responds to a --help switch by dispatching a help event', function () { 58 | var callback = sinon.spy(), 59 | cliController = new CliController(); 60 | 61 | cliController.on('showHelp', callback); 62 | cliController.dispatch({'h': true}); 63 | expect(callback).to.have.been.called; 64 | }); 65 | 66 | it('responds to a -f switch by dispatching a format event', function () { 67 | var callback = sinon.spy(), 68 | cliController = new CliController(); 69 | 70 | cliController.on('setFormat', callback); 71 | cliController.dispatch({'f': 'json'}); 72 | expect(callback).to.have.been.calledWith('json'); 73 | }); 74 | 75 | it('responds to a --format switch by dispatching a format event', function () { 76 | var callback = sinon.spy(), 77 | cliController = new CliController(); 78 | 79 | cliController.on('setFormat', callback); 80 | cliController.dispatch({'format': 'json'}); 81 | expect(callback).to.have.been.calledWith('json'); 82 | }); 83 | 84 | it('responds to no data supplied by dispatching a help event', function () { 85 | var callback = sinon.spy(), 86 | cliController = new CliController(); 87 | 88 | cliController.on('showHelp', callback); 89 | cliController.dispatch({}); 90 | expect(callback).to.have.been.called; 91 | }); 92 | 93 | it('responds to unnamed arguments by dispatching file event, then a run event', function () { 94 | var pathsCallback = sinon.spy(), 95 | cliController = new CliController(); 96 | 97 | cliController.on('runPaths', pathsCallback); 98 | cliController.dispatch({'_': ['styles.css', 'ie.css']}); 99 | expect(pathsCallback).to.have.been.calledWith(['styles.css', 'ie.css']); 100 | }); 101 | }); -------------------------------------------------------------------------------- /test/CssDeclaration.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | var expect = require('chai').expect, 4 | CssDeclaration = require('../lib/CssDeclaration.js'); 5 | 6 | describe('The Declaration Parser', function() { 7 | beforeEach(function() { 8 | cssDeclaration = new CssDeclaration('color: #fff'); 9 | }); 10 | 11 | it('should return a property and a value for a declaration', function() { 12 | expect(cssDeclaration.getProperty()).to.equal('color'); 13 | expect(cssDeclaration.getValue()).to.equal('#fff'); 14 | }); 15 | }); -------------------------------------------------------------------------------- /test/CssMediaQuery.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | var expect = require('chai').expect, 4 | CssMediaQuery = require('../lib/CssMediaQuery.js'); 5 | 6 | describe('The Css Media Query object', function () { 7 | it('should return (n) queries for a media query of (n) queries', function () { 8 | var cssMediaQuery = new CssMediaQuery('@media print {a {color: #000;}}'); 9 | expect(cssMediaQuery.getQueries()[0]).to.equal('print'); 10 | 11 | cssMediaQuery = new CssMediaQuery('@media (min-width: 700px) and (orientation: landscape) {.blogroll {display: none;}}'); 12 | expect(cssMediaQuery.getQueries()[0]).to.equal('(min-width: 700px) and (orientation: landscape)'); 13 | 14 | cssMediaQuery = new CssMediaQuery('@media handheld, (min-width: 700px) {.blogroll {display: none;}}'); 15 | expect(cssMediaQuery.getQueries()[0]).to.equal('handheld'); 16 | expect(cssMediaQuery.getQueries()[1]).to.equal('(min-width: 700px)'); 17 | 18 | cssMediaQuery = new CssMediaQuery('@media handheld or (min-width: 700px) {.blogroll {display: none;}}'); 19 | expect(cssMediaQuery.getQueries()[0]).to.equal('handheld'); 20 | expect(cssMediaQuery.getQueries()[1]).to.equal('(min-width: 700px)'); 21 | }); 22 | 23 | it('should return (n) queries for a media query of (n) rules', function () { 24 | var cssMediaQuery = new CssMediaQuery('@media print {a {color: #000;} header {display: none;}}'); 25 | expect(cssMediaQuery.getRules()[0]).to.equal('a {color: #000;}'); 26 | expect(cssMediaQuery.getRules()[1]).to.equal('header {display: none;}'); 27 | }); 28 | }); -------------------------------------------------------------------------------- /test/CssRule.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | var expect = require('chai').expect, 4 | CssRule = require('../lib/CssRule.js'); 5 | 6 | describe('The Rule Parser', function () { 7 | it('should return a collection of (n) items of selectors for a rule of (n) selectors', function () { 8 | var cssRule = new CssRule('body {border: 0;}'); 9 | expect(cssRule.getSelectors()).to.have.length(1); 10 | 11 | var complexRule = 'body, html, .container .wrapper, #container, .container a[rel="blah"]' 12 | + '{background: url(img.png); background-color: #ffffff;}'; 13 | cssRule = new CssRule(complexRule); 14 | 15 | expect(cssRule.getSelectors()).to.have.length(5); 16 | }); 17 | 18 | it('should return a collection of (n) items of declarations for a rule of (n) declarations', function () { 19 | var cssRule = new CssRule('body {border: 0;}'); 20 | expect(cssRule.getDeclarations()).to.have.length(1); 21 | 22 | var complexRule = 'body, html, .container .wrapper, #container, .container a[rel="blah"]' 23 | + '{background: url(img.png); background-color: linear-gradient(45deg, #00f, #f00)}'; 24 | cssRule = new CssRule(complexRule); 25 | 26 | expect(cssRule.getDeclarations()).to.have.length(2); 27 | }); 28 | }); -------------------------------------------------------------------------------- /test/CssSelector.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | var expect = require('chai').expect, 4 | CssSelector = require('../lib/CssSelector.js'); 5 | 6 | describe('The Selector Parser', function() { 7 | it('returns a collection of (n) items for a selector of (n) identifiers', function() { 8 | var cssSelector = new CssSelector('#my-form'); 9 | expect(cssSelector.getIdentifiers()).to.have.length(1); 10 | cssSelector = new CssSelector('#my-form #username.error'); 11 | expect(cssSelector.getIdentifiers()).to.have.length(3); 12 | cssSelector = new CssSelector('#my-form input[name=username]'); 13 | expect(cssSelector.getIdentifiers()).to.have.length(3); 14 | }); 15 | 16 | it('correctly parses universal identifiers in a selector', function() { 17 | var cssSelector = new CssSelector('body .list *:first-child'); 18 | expect(cssSelector.getIdentifiers()).to.have.length(4); 19 | }); 20 | 21 | it('correctly parses type identifiers in a selector', function() { 22 | var cssSelector = new CssSelector('body form input'); 23 | expect(cssSelector.getIdentifiers()).to.have.length(3); 24 | }); 25 | 26 | it('correctly parses class identifiers in a selector', function() { 27 | var cssSelector = new CssSelector('.login-form .checkbox.error'); 28 | expect(cssSelector.getIdentifiers()).to.have.length(3); 29 | }); 30 | 31 | it('correctly parses id identifiers in a selector', function() { 32 | var cssSelector = new CssSelector('#login-page #form#login-form input#password.error'); 33 | expect(cssSelector.getIdentifiers()).to.have.length(6); 34 | }); 35 | 36 | it('correctly parses attribute identifiers in a selector', function() { 37 | var cssSelector = new CssSelector('form[name=login-form]'); 38 | expect(cssSelector.getIdentifiers()).to.have.length(2); 39 | cssSelector = new CssSelector('a[rel][]'); 40 | expect(cssSelector.getIdentifiers()).to.have.length(2); 41 | cssSelector = new CssSelector('a[rel~="copyright"]'); 42 | expect(cssSelector.getIdentifiers()).to.have.length(2); 43 | cssSelector = new CssSelector('a[hreflang|="en"]'); 44 | expect(cssSelector.getIdentifiers()).to.have.length(2); 45 | cssSelector = new CssSelector('a[hreflang=fr]'); 46 | expect(cssSelector.getIdentifiers()).to.have.length(2); 47 | }); 48 | 49 | it('correctly parses pseudo-class identifiers in a selector', function() { 50 | var cssSelector = new CssSelector('a:first-child'); 51 | expect(cssSelector.getIdentifiers()).to.have.length(2); 52 | }); 53 | 54 | it('correctly parses pseudo-element identifiers in a selector', function() { 55 | var cssSelector = new CssSelector('p::first-line'); 56 | expect(cssSelector.getIdentifiers()).to.have.length(2); 57 | }); 58 | }); -------------------------------------------------------------------------------- /test/CssStylesheet.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | var expect = require('chai').expect, 4 | CssStylesheet = require('../lib/CssStylesheet.js'); 5 | 6 | describe('The Stylesheet Parser', function() { 7 | it('should return an empty array of child rules for an empty string', function () { 8 | var cssStylesheet = new CssStylesheet(''); 9 | expect(cssStylesheet.getRules()).to.be.an('array'); 10 | expect(cssStylesheet.getRules()).to.be.empty; 11 | }); 12 | 13 | it('should return (n) items of child rules for a stylesheet of (n) rules', function() { 14 | var cssStylesheet = new CssStylesheet("body {border: 0} h1 {font-weight: bold;}"); 15 | expect(cssStylesheet.getRules()).to.have.length(2); 16 | 17 | var multipleRules = "body {border: 0;} h1 {font-weight: bold;}" 18 | + " p {color: #333333; font-size: 1.2em; width: 100%} input {border: 1px solid #333333;" 19 | + " background-color: 1px solid #CCCCCC; padding: 0.1em; linear-gradient(45deg, #00f, #f00)}"; 20 | cssStylesheet = new CssStylesheet(multipleRules); 21 | expect(cssStylesheet.getRules()).to.have.length(4); 22 | 23 | var multipleSelectors = "body section :first-child {background: #FFF;}" 24 | + "form#registration-form > input.username {font-weight: bold;}"; 25 | cssStylesheet = new CssStylesheet(multipleSelectors); 26 | expect(cssStylesheet.getRules()).to.have.length(2); 27 | }); 28 | 29 | it('should distinguish between media-queries and at-rules', function () { 30 | var cssStylesheet = new CssStylesheet("@media screen { body {border: 0;} } @include (styles.css); body { margin: 0; }"); 31 | expect(cssStylesheet.getMediaQueries()[0]).to.equal('@media screen { body {border: 0;} }'); 32 | expect(cssStylesheet.getMediaQueries()).to.have.length(1); 33 | expect(cssStylesheet.getRules()[0]).to.equal('body { margin: 0; }'); 34 | }); 35 | 36 | it('should return (n) items of child media-queries for a stylesheet of (n) media-queries', function () { 37 | var cssStylesheet = new CssStylesheet("@media screen { body {border: 0;} } @media print { a {color: #000;} } @include (styles.css);"); 38 | expect(cssStylesheet.getMediaQueries()).to.have.length(2); 39 | expect(cssStylesheet.getMediaQueries()[0]).to.equal('@media screen { body {border: 0;} }'); 40 | expect(cssStylesheet.getMediaQueries()[1]).to.equal('@media print { a {color: #000;} }'); 41 | }); 42 | 43 | it('should ignore comments', function () { 44 | var cssStylesheet = new CssStylesheet("body {border: 0;} /* ./* */ a {color: #fff; /* comment */} * {margin: 0;}"); 45 | expect(cssStylesheet.getRules()).to.have.length(3); 46 | }); 47 | 48 | it('should ignore newline characters', function () { 49 | var cssStylesheet = new CssStylesheet("body {\nborder: 0;\n}\na{\ncolor: #fff;\n}"); 50 | expect(cssStylesheet.getRules()[0]).to.equal("body {border: 0;}"); 51 | expect(cssStylesheet.getRules()[1]).to.equal("a{color: #fff;}"); 52 | var cssStylesheet = new CssStylesheet("body {\r\nborder: 0;\r\n}\r\na{\r\ncolor: #fff;\r\n}"); 53 | expect(cssStylesheet.getRules()[0]).to.equal("body {border: 0;}"); 54 | expect(cssStylesheet.getRules()[1]).to.equal("a{color: #fff;}"); 55 | }); 56 | 57 | it('should return an array of malformed statements for a string containing malformed statements', function () { 58 | var cssStylesheet = new CssStylesheet('body {border: 0;} h1; h2 {font-weight: bold;}'); 59 | expect(cssStylesheet.getRules()).to.be.an('array'); 60 | expect(cssStylesheet.getMalformedStatements()[0]).to.equal('h1;'); 61 | }); 62 | 63 | it('should identify a rule with an unexpected colon as a malformed statement', function () { 64 | var cssStylesheet = new CssStylesheet('body {border: 0;} h1;{font-weight: bold;}'); 65 | expect(cssStylesheet.getRules()).to.be.an('array'); 66 | expect(cssStylesheet.getMalformedStatements()[0]).to.equal('h1;'); 67 | expect(cssStylesheet.getMalformedStatements()[1]).to.equal('{font-weight: bold;}'); 68 | }); 69 | }); -------------------------------------------------------------------------------- /test/DeclarationsPerRule.js: -------------------------------------------------------------------------------- 1 | 2 | /*! Parker v0.0.0 - MIT license */ 3 | 4 | var expect = require('chai').expect, 5 | metric = require('../metrics/DeclarationsPerRule.js'); 6 | 7 | 8 | describe('The declarations-per-rule metric', function () { 9 | it('should provide a string identifier for the metric', function() { 10 | expect(metric.id).to.be.a('string'); 11 | }); 12 | 13 | it('should provide a metric type', function() { 14 | expect(metric.aggregate).to.match(/sum|mean/g); 15 | }); 16 | 17 | it('should return 0 for an empty string', function () { 18 | expect(metric.measure('')).to.equal(0); 19 | }); 20 | 21 | it('should return 1 for the selector "body { color: #222 }"', function() { 22 | expect(metric.measure('body { color: 1 }')).to.equal(1); 23 | }); 24 | 25 | it('should return 2 for the selector "body { color: #222; backgrund: #333; }"', function() { 26 | expect(metric.measure('body { color: #222; backgrund: #333; }')).to.equal(2); 27 | }); 28 | 29 | it('should return 3 for the selector "body { color: #222; backgrund: #333; margin: 0; }"', function() { 30 | expect(metric.measure('body { color: #222; backgrund: #333; margin: 0; }')).to.equal(3); 31 | }); 32 | 33 | it('should return 4 for the selector "body { color: #222; backgrund: #333; margin: 0; padding: 0; }"', function() { 34 | expect(metric.measure('body { color: #222; backgrund: #333; margin: 0; padding: 0; }')).to.equal(4); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/IdentifiersPerSelector.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | var expect = require('chai').expect, 4 | metric = require('../metrics/IdentifiersPerSelector.js'); 5 | 6 | 7 | describe('The identifiers-per-selector metric', function () { 8 | it('should provide a string identifier for the metric', function() { 9 | expect(metric.id).to.be.a('string'); 10 | }); 11 | 12 | it('should provide a metric type', function() { 13 | expect(metric.aggregate).to.match(/sum|mean/g); 14 | }); 15 | 16 | it('should return 0 for an empty string', function () { 17 | expect(metric.measure('')).to.equal(0); 18 | }); 19 | 20 | it('should return 1 for the selector "body"', function() { 21 | expect(metric.measure('body')).to.equal(1); 22 | }); 23 | 24 | it('should return 2 for the selector "body section"', function() { 25 | expect(metric.measure('body section')).to.equal(2); 26 | }); 27 | 28 | it('should return 3 for the selector "body section.articles"', function() { 29 | expect(metric.measure('body section.acticles')).to.equal(3); 30 | }); 31 | 32 | it('should return 4 for the selector "body section.articles>article"', function() { 33 | expect(metric.measure('body section.articles>article')).to.equal(4); 34 | }); 35 | 36 | it('should return 5 for the selector "body section.articles>article:first-child"', function() { 37 | expect(metric.measure('body section.articles>article:first-child')).to.equal(5); 38 | }) 39 | 40 | it('should return 7 for the selector "body section.articles>article:first-child p::first-line', function() { 41 | expect(metric.measure('body section.articles>article:first-child p::first-line')).to.equal(7); 42 | }); 43 | 44 | it('should return 3 for the selector "form[name=login] input"', function() { 45 | expect(metric.measure('form[name=login] input')).to.equal(3); 46 | expect(metric.measure('form[name="login"] input')).to.equal(3); 47 | expect(metric.measure("form[name='login'] input")).to.equal(3); 48 | }); 49 | 50 | it('should return 2 for the selector "input[checked]"', function() { 51 | expect(metric.measure('input[checked]')).to.equal(2); 52 | }); 53 | 54 | it('should return 2 for the selector "input[value~=Mrs]"', function() { 55 | expect(metric.measure('input[value~=Mrs]')).to.equal(2); 56 | expect(metric.measure('input[value~="Mrs"]')).to.equal(2); 57 | expect(metric.measure("input[value~='Mrs']")).to.equal(2); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/Parker.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | var expect = require('chai').expect, 4 | Parker = require('../lib/Parker.js'); 5 | 6 | describe('The Parker tool', function() { 7 | it('should throw an exception if stylesheet data not supplied as string, array or multi-param strings', function() { 8 | parker = new Parker([]); 9 | expect(function() {parker.run({})}).to.throw(); 10 | expect(function() {parker.run(0)}).to.throw(); 11 | expect(function() {parker.run(1, [], {})}).to.throw(); 12 | }); 13 | 14 | it('should not throw an exception if stylesheet data is supplied as string, array or multi-param strings', function() { 15 | parker = new Parker([]); 16 | expect(function() {parker.run('body {background: #FFF;}')}); 17 | expect(function() {parker.run(array('body {background: #FFF;}'))}); 18 | expect(function() {parker.run('body {background: #FFF;}', 'body {background: #FFF;}')}); 19 | }); 20 | 21 | it('should run metrics on stylesheets', function() { 22 | var mockMetric = {id: 'mock-stylesheet-metric', type: 'stylesheet', aggregate: 'sum', measure: function() {return 1}}; 23 | parker = new Parker([mockMetric]); 24 | expect(parker.run('body {background: #FFF;}')).to.have.property('mock-stylesheet-metric'); 25 | }); 26 | 27 | it('should return sum values for sum metrics', function() { 28 | var mockMetric = {id: 'mock-stylesheet-metric', type: 'stylesheet', aggregate: 'sum', measure: function() {return 1}}, 29 | stylesheet = 'body {background: #FFF;}'; 30 | 31 | parker = new Parker([mockMetric]); 32 | var report = parker.run([stylesheet, stylesheet, stylesheet]); 33 | 34 | 35 | expect(report).to.have.property('mock-stylesheet-metric'); 36 | expect(report['mock-stylesheet-metric']).to.equal(3); 37 | }); 38 | 39 | it('should return 0 for sum metrics with no data to report on', function() { 40 | var mockMetric = {id: 'mock-stylesheet-metric', type: 'rule', aggregate: 'sum', measure: function() {return 1}}, 41 | stylesheet = '/* comment */'; 42 | 43 | parker = new Parker([mockMetric]); 44 | var report = parker.run([stylesheet, stylesheet, stylesheet]); 45 | 46 | 47 | expect(report).to.have.property('mock-stylesheet-metric'); 48 | expect(report['mock-stylesheet-metric']).to.equal(0); 49 | }); 50 | 51 | it('should return mean values for mean metrics', function() { 52 | var mockMetric = {id: 'mock-stylesheet-metric', type: 'stylesheet', aggregate: 'mean', measure: function() {return 1}}, 53 | stylesheet = 'body {background: #FFF;}'; 54 | 55 | parker = new Parker([mockMetric]); 56 | var report = parker.run([stylesheet, stylesheet, stylesheet]); 57 | 58 | 59 | expect(report).to.have.property('mock-stylesheet-metric'); 60 | expect(report['mock-stylesheet-metric']).to.equal(1); 61 | }); 62 | 63 | it('should return 0 for mean metrics with no data to report on', function () { 64 | var mockMetric = {id: 'mock-stylesheet-metric', type: 'rule', aggregate: 'mean', measure: function() {return 1}}, 65 | stylesheet = '/* comment */'; 66 | 67 | parker = new Parker([mockMetric]); 68 | var report = parker.run([stylesheet]); 69 | 70 | 71 | expect(report).to.have.property('mock-stylesheet-metric'); 72 | expect(report['mock-stylesheet-metric']).to.equal(0); 73 | }); 74 | 75 | it('should return max values for max metrics', function () { 76 | var mockIntMetric = {id: 'mock-int-metric', type: 'stylesheet', aggregate: 'max', measure: function(stylesheet) {return stylesheet.length;}}, 77 | parker = new Parker([mockIntMetric]), 78 | report = parker.run(['body {background: #FFF;}', 'body {background: #FFFFFF;}']); 79 | 80 | expect(report).to.have.property('mock-int-metric'); 81 | expect(report['mock-int-metric']).to.equal(27); 82 | }); 83 | 84 | it('should return 0 for max metrics with no data to report on', function () { 85 | var mockIntMetric = {id: 'mock-int-metric', type: 'rule', aggregate: 'max', measure: function(stylesheet) {return stylesheet.length;}}, 86 | parker = new Parker([mockIntMetric]), 87 | report = parker.run(['/* comment */']); 88 | 89 | expect(report).to.have.property('mock-int-metric'); 90 | expect(report['mock-int-metric']).to.equal(0); 91 | }); 92 | 93 | it('should return max values determined by an iterator function if one is present', function() { 94 | var mockStringMetric = {id: 'mock-string-metric', type: 'stylesheet', aggregate: 'max', measure: function(stylesheet) {return stylesheet;}, iterator: function(string) {return string.length}}, 95 | parker = new Parker([mockStringMetric]), 96 | report = parker.run(['body {background: #FFF;}', 'body {background: #FFFFFF;}']); 97 | 98 | expect(report).to.have.property('mock-string-metric'); 99 | expect(report['mock-string-metric']).to.equal('body {background: #FFFFFF;}'); 100 | }); 101 | 102 | it('should return list values for list metrics', function () { 103 | var mockSingleMetric = {id: 'mock-single-list-item-metric', type: 'stylesheet', aggregate: 'list', measure: function(stylesheet) {return stylesheet}}, 104 | mockMultipleMetric = {id: 'mock-multiple-list-item-metric', type: 'stylesheet', aggregate: 'list', measure: function(stylesheet) {return ['a', 'b']}}, 105 | parker = new Parker([mockSingleMetric, mockMultipleMetric]), 106 | report = parker.run(['body {background: #FFF;}', 'body {background: #FFF;}']); 107 | 108 | expect(report).to.have.property('mock-single-list-item-metric'); 109 | expect(report['mock-single-list-item-metric']).to.be.an('array'); 110 | expect(report['mock-single-list-item-metric']).to.have.length(2); 111 | expect(report['mock-single-list-item-metric'][0]).to.equal('body {background: #FFF;}'); 112 | expect(report['mock-single-list-item-metric'][1]).to.equal('body {background: #FFF;}'); 113 | expect(report['mock-multiple-list-item-metric']).to.be.an('array'); 114 | expect(report['mock-multiple-list-item-metric']).to.have.length(4); 115 | expect(report['mock-multiple-list-item-metric'][0]).to.equal('a'); 116 | expect(report['mock-multiple-list-item-metric'][1]).to.equal('b'); 117 | expect(report['mock-multiple-list-item-metric'][0]).to.equal('a'); 118 | expect(report['mock-multiple-list-item-metric'][1]).to.equal('b'); 119 | }); 120 | 121 | it('should return list values filtered by a filter function if one is present', function() { 122 | var mockMetric = {id: 'mock-list-metric', type: 'stylesheet', aggregate: 'list', measure: function(stylesheet) {return stylesheet}, filter: function(value, index, self) {return self.indexOf(value) === index;}}, 123 | parker = new Parker([mockMetric]), 124 | report = parker.run(['body {background: #FFF;}', 'body {background: #FFF;}']); 125 | 126 | expect(report).to.have.property('mock-list-metric'); 127 | expect(report['mock-list-metric']).to.be.an('array'); 128 | expect(report['mock-list-metric']).to.have.length(1); 129 | expect(report['mock-list-metric'][0]).to.equal('body {background: #FFF;}'); 130 | }); 131 | 132 | it('should return length values for length metrics', function () { 133 | var mockMetric = {id: 'mock-length-metric', type: 'stylesheet', aggregate: 'length', measure: function(stylesheet) {return stylesheet}}, 134 | parker = new Parker([mockMetric]); 135 | report = parker.run(['body {background: #FFF;}', 'body {background: #FFF;}', '']); 136 | 137 | expect(report).to.have.property('mock-length-metric'); 138 | expect(report['mock-length-metric']).to.equal(2); 139 | }); 140 | 141 | it('should run metrics on rules', function() { 142 | var mockMetric = {id: 'mock-rule-metric', type: 'rule', aggregate: 'sum', measure: function() {return 1}}; 143 | parker = new Parker([mockMetric]), 144 | report = parker.run('body {background: #FFF;} h1 {font-weight: bold;}'); 145 | 146 | expect(report).to.have.property('mock-rule-metric'); 147 | expect(report['mock-rule-metric']).to.equal(2); 148 | }); 149 | 150 | it('should run metrics on media queries', function () { 151 | var mockMetric = {id: 'mock-media-query-metric', type: 'mediaquery', aggregate: 'list', measure: function(query) {return query;}}; 152 | parker = new Parker([mockMetric]), 153 | report = parker.run('@media handheld, (max-width: 700px) { body { margin: 100px; }} @import url(css/styles.css); body { margin: 0; }'); 154 | expect(report).to.have.property('mock-media-query-metric'); 155 | expect(report['mock-media-query-metric'][0]).to.equal('handheld'); 156 | expect(report['mock-media-query-metric'][1]).to.equal('(max-width: 700px)'); 157 | }); 158 | 159 | it('should run metrics on rules inside media query blocks', function () { 160 | var mockMetric = {id: 'mock-media-query-metric', type: 'rule', aggregate: 'list', measure: function(rule) {return rule;}}, 161 | parker = new Parker([mockMetric]), 162 | report = parker.run('@media print {a {color: #000;} header {display: none;}}'); 163 | expect(report).to.have.property('mock-media-query-metric'); 164 | expect(report['mock-media-query-metric'][0]).to.equal('a {color: #000;}'); 165 | expect(report['mock-media-query-metric'][1]).to.equal('header {display: none;}'); 166 | }); 167 | 168 | it('should run metrics on selectors', function() { 169 | var mockMetric = {id: 'mock-selector-metric', type: 'selector', aggregate: 'sum', measure: function() {return 1}}; 170 | parker = new Parker([mockMetric]); 171 | report = parker.run('body section {background: #FFF;} h1 {font-weight: bold;}'); 172 | 173 | expect(report).to.have.property('mock-selector-metric'); 174 | expect(report['mock-selector-metric']).to.equal(2); 175 | }); 176 | 177 | it('should run metrics on identifiers', function() { 178 | var mockMetric = {id: 'mock-identifier-metric', type: 'identifier', aggregate: 'sum', measure: function() {return 1}}; 179 | parker = new Parker([mockMetric]); 180 | 181 | report = parker.run("body section :first-child {background: #FFF;} form#registration-form > input.username {font-weight: bold;}"); 182 | expect(report).to.have.property('mock-identifier-metric'); 183 | expect(report['mock-identifier-metric']).to.equal(7); 184 | }); 185 | 186 | it('should run metrics on declarations', function() { 187 | var mockMetric = {id: 'mock-declaration-metric', type: 'declaration', aggregate: 'sum', measure: function() {return 1}}; 188 | parker = new Parker([mockMetric]); 189 | 190 | report = parker.run("body {margin: 0; padding: 0} a {color: #00f} h1 {font-weight: bold; color: #000;}"); 191 | expect(report).to.have.property('mock-declaration-metric'); 192 | expect(report['mock-declaration-metric']).to.equal(5); 193 | }); 194 | 195 | it('should run metrics on properties', function() { 196 | var mockMetric = {id: 'mock-property-metric', type: 'property', aggregate: 'sum', measure: function() {return 1}}; 197 | parker = new Parker([mockMetric]); 198 | 199 | report = parker.run("body {margin: 0; padding: 0} a {color: #00f} h1 {font-weight: bold; color: #000;}"); 200 | expect(report).to.have.property('mock-property-metric'); 201 | expect(report['mock-property-metric']).to.equal(5); 202 | }); 203 | 204 | it('should run metrics on values', function() { 205 | var mockMetric = {id: 'mock-value-metric', type: 'value', aggregate: 'sum', measure: function() {return 1}}; 206 | parker = new Parker([mockMetric]); 207 | 208 | report = parker.run("body {margin: 0; padding: 0} a {color: #00f} h1 {font-weight: bold; color: #000;}"); 209 | expect(report).to.have.property('mock-value-metric'); 210 | expect(report['mock-value-metric']).to.equal(5); 211 | }); 212 | 213 | it('should return results for metrics measuring optional elements when those elements are not found', function () { 214 | var mockMetric = {id: 'mock-media-query-metric', type: 'mediaquery', aggregate: 'length', measure: function(query) {return query;}}; 215 | parker = new Parker([mockMetric]), 216 | report = parker.run('body { margin: 0; }'); 217 | 218 | expect(report).to.have.property('mock-media-query-metric'); 219 | expect(report['mock-media-query-metric']).to.equal(0); 220 | }); 221 | }); -------------------------------------------------------------------------------- /test/TopSelectorSpecificity.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | metric = require('../metrics/TopSelectorSpecificity.js'); 3 | 4 | describe('The top-selector-specificity metric', function () { 5 | it('should return a specificity of "1" for the selector "table"', function () { 6 | expect(metric.measure("table")).to.equal(1); 7 | }); 8 | 9 | it('should return a specificity of "1" for the selector "::first-line"', function () { 10 | expect(metric.measure('::before')).to.equal(1); 11 | }); 12 | 13 | it('should return a specificity of "10" for the selector ".class"', function () { 14 | expect(metric.measure('.class')).to.equal(10); 15 | }); 16 | 17 | it('should return a specificity of "10" for the selector "[href="/"]"', function () { 18 | expect(metric.measure('[href="/"]')).to.equal(10); 19 | }); 20 | 21 | it('should return a specificity of "10" for the selector ":first"', function () { 22 | expect(metric.measure(':first')).to.equal(10); 23 | }); 24 | 25 | it('should return a specificity of "100" for the selector "#main"', function () { 26 | expect(metric.measure('#main')).to.equal(100); 27 | }); 28 | 29 | it('should return a specificity of "0" for the selector "*"', function () { 30 | expect(metric.measure('*')).to.equal(0); 31 | }); 32 | 33 | it('should count only the specificity of the child selector of a :not identifier', function () { 34 | expect(metric.measure(':not(body)')).to.equal(1); 35 | expect(metric.measure(':not(.sidebar)')).to.equal(10); 36 | expect(metric.measure(':not(h1#main)')).to.equal(101); 37 | }); 38 | 39 | it('should ignore identifier tokens inside attribute selectors', function () { 40 | expect(metric.measure('[href="#main"]')).to.equal(10); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/TotalIdSelectors.js: -------------------------------------------------------------------------------- 1 | /*! Parker v0.0.0 - MIT license */ 2 | 3 | var expect = require('chai').expect, 4 | metric = require('../metrics/TotalIdSelectors.js'); 5 | 6 | describe('The total-id-selectors metric', function () { 7 | it('should provide a string identifier for the metric', function() { 8 | expect(metric.id).to.be.a('string'); 9 | }); 10 | 11 | it('should provide a metric type', function() { 12 | expect(metric.aggregate).to.match(/sum|mean/g); 13 | }); 14 | 15 | it('should return 0 for an empty string', function () { 16 | expect(metric.measure('')).to.equal(0); 17 | }); 18 | 19 | it('should return 1 for the selector "#test"', function() { 20 | expect(metric.measure('#test')).to.equal(1); 21 | }); 22 | 23 | it('should return 1 for the selector "foo#test"', function() { 24 | expect(metric.measure('foo#test')).to.equal(1); 25 | }); 26 | 27 | it('should return 1 for the selector "foo #test"', function() { 28 | expect(metric.measure('foo #test')).to.equal(1); 29 | }); 30 | 31 | it('should return 0 for the selector "[href="#foo"]"', function () { 32 | expect(metric.measure('[href="#foo"]')).to.equal(0); 33 | }); 34 | 35 | it('should return 1 for the selector "[href="#foo"] #foo"', function () { 36 | expect(metric.measure('[href="#foo"] #foo')).to.equal(1); 37 | }); 38 | 39 | it('should return 2 for the selector "#foo #bar"', function () { 40 | expect(metric.measure('#foo #bar')).to.equal(2); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/ZIndexes.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | metric = require('../metrics/ZIndexes.js'); 3 | 4 | describe("The Z-Indexes metric", function() { 5 | it('should return 1 for "z-index: 1"', function () { 6 | expect(metric.measure('z-index: 1')).to.equal('1'); 7 | }); 8 | }); 9 | --------------------------------------------------------------------------------