├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── __tests__ ├── index.js └── test_examples │ ├── attribute_selector │ ├── attribute_selector.css │ └── attribute_selector.html │ ├── bootstrap │ └── modified-bootstrap.css │ ├── camel_case │ ├── camel_case.css │ └── camel_case.js │ ├── combined │ ├── combined.css │ └── combined.js │ ├── delimited │ ├── delimited.css │ └── delimited.html │ ├── media_queries │ ├── media_queries.css │ └── media_queries.html │ ├── multiple_files │ ├── multiple_files.css │ ├── multiple_files.html │ ├── multiple_files.js │ └── multiple_files2.css │ ├── pseudo_class │ ├── pseudo_class.css │ └── pseudo_class.js │ ├── remove_unused │ ├── remove_unused.css │ └── remove_unused.js │ ├── simple │ ├── simple.css │ └── simple.js │ ├── special │ ├── special.css │ └── special.js │ ├── special_characters │ ├── special_characters.css │ └── special_characters.js │ └── wildcard │ ├── wildcard.css │ └── wildcard.html ├── bin └── purifycss ├── config └── rollup.config.js ├── lib ├── purifycss.es.js └── purifycss.js ├── package-lock.json ├── package.json ├── purified.css └── src ├── CssTreeWalker.js ├── SelectorFilter.js ├── purifycss.js └── utils ├── ExtractWordsUtil.js ├── FileUtil.js └── PrintUtil.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "sourceType": "module", 5 | "ecmaFeatures": { 6 | "jsx": true 7 | } 8 | }, 9 | "env": { 10 | "browser": true, 11 | "commonjs": true, 12 | "node": true, 13 | "es6": true 14 | }, 15 | "rules": { 16 | "no-cond-assign": ["error", "always"], 17 | "no-console": ["error"], 18 | "no-constant-condition": ["error"], 19 | "no-control-regex": ["error"], 20 | "no-debugger": ["warn"], 21 | "no-dupe-args": ["error"], 22 | "no-dupe-keys": ["error"], 23 | "no-duplicate-case": ["error"], 24 | "no-empty": ["error"], 25 | "no-empty-character-class": ["error"], 26 | "no-ex-assign": ["error"], 27 | "no-extra-boolean-cast": ["error"], 28 | "no-extra-parens": ["error"], 29 | "no-extra-semi": ["error"], 30 | "no-func-assign": ["error"], 31 | "no-inner-declarations": ["error"], 32 | "no-invalid-regexp": ["error"], 33 | "no-irregular-whitespace": ["error"], 34 | "no-negated-in-lhs": ["error"], 35 | "no-obj-calls": ["error"], 36 | "no-prototype-builtins": ["error"], 37 | "no-regex-spaces": ["error"], 38 | "no-sparse-arrays": ["error"], 39 | "no-unexpected-multiline": ["error"], 40 | "no-unreachable": ["error"], 41 | "no-unsafe-finally": ["error"], 42 | "use-isnan": ["error"], 43 | "valid-jsdoc": ["off"], 44 | "valid-typeof": ["error"], 45 | 46 | "accessor-pairs": ["error"], 47 | "array-callback-return": ["error"], 48 | "block-scoped-var": ["error"], 49 | "complexity": ["error", 30], 50 | "consistent-return": ["error"], 51 | "curly": ["off", "all"], 52 | "default-case": ["error"], 53 | "dot-location": ["off"], 54 | "dot-notation": ["error"], 55 | "eqeqeq": ["error", "always"], 56 | "guard-for-in": ["warn"], 57 | "no-alert": ["error"], 58 | "no-caller": ["error"], 59 | "no-case-declarations": ["error"], 60 | "no-div-regex": ["error"], 61 | "no-else-return": ["error"], 62 | "no-empty-function": ["error"], 63 | "no-empty-pattern": ["error"], 64 | "no-eq-null": ["error"], 65 | "no-eval": ["error"], 66 | "no-extend-native": ["error"], 67 | "no-extra-bind": ["error"], 68 | "no-extra-label": ["error"], 69 | "no-fallthrough": ["error"], 70 | "no-floating-decimal": ["error"], 71 | "no-implicit-coercion": ["error"], 72 | "no-implicit-globals": ["error"], 73 | "no-implied-eval": ["error"], 74 | "no-invalid-this": ["error"], 75 | "no-iterator": ["error"], 76 | "no-labels": ["error"], 77 | "no-lone-blocks": ["error"], 78 | "no-loop-func": ["error"], 79 | "no-magic-numbers": ["off"], 80 | "no-multi-spaces": ["error"], 81 | "no-multi-str": ["error"], 82 | "no-native-reassign": ["error"], 83 | "no-new": ["error"], 84 | "no-new-func": ["error"], 85 | "no-new-wrappers": ["error"], 86 | "no-octal": ["error"], 87 | "no-octal-escape": ["error"], 88 | "no-param-reassign": ["error"], 89 | "no-proto": ["error"], 90 | "no-redeclare": ["error"], 91 | "no-return-assign": ["error"], 92 | "no-script-url": ["error"], 93 | "no-self-assign": ["error"], 94 | "no-self-compare": ["error"], 95 | "no-sequences": ["error"], 96 | "no-throw-literal": ["error"], 97 | "no-unmodified-loop-condition": ["error"], 98 | "no-unused-expressions": ["error"], 99 | "no-unused-labels": ["error"], 100 | "no-useless-call": ["error"], 101 | "no-useless-concat": ["error"], 102 | "no-useless-escape": ["error"], 103 | "no-void": ["error"], 104 | "no-warning-comments": ["warn"], 105 | "no-with": ["error"], 106 | "radix": ["error"], 107 | "vars-on-top": ["error"], 108 | "wrap-iife": ["error"], 109 | "yoda": ["error", "never"], 110 | 111 | "strict": ["off", "safe"], 112 | "init-declarations": ["off", "never"], 113 | "no-catch-shadow": ["error"], 114 | "no-delete-var": ["error"], 115 | "no-label-var": ["error"], 116 | "no-restricted-globals": ["error"], 117 | "no-shadow": ["error"], 118 | "no-shadow-restricted-names": ["error"], 119 | "no-undef": ["error"], 120 | "no-undef-init": ["error"], 121 | "no-undefined": ["error"], 122 | "no-unused-vars": ["error"], 123 | "no-use-before-define": ["error"], 124 | 125 | "callback-return": ["error"], 126 | "global-require": ["error"], 127 | "handle-callback-err": ["error"], 128 | "no-mixed-requires": ["error"], 129 | "no-new-require": ["error"], 130 | "no-path-concat": ["error"], 131 | "no-process-env": ["error"], 132 | "no-process-exit": ["error"], 133 | "no-restricted-modules": ["error"], 134 | "no-sync": ["error"], 135 | 136 | "array-bracket-spacing": ["error", "never"], 137 | "block-spacing": ["error", "never"], 138 | "brace-style": ["error", "1tbs"], 139 | "camelcase": ["error"], 140 | "comma-dangle": ["error", "never"], 141 | "comma-spacing": ["error", { "before": false, "after": true }], 142 | "comma-style": ["error", "last"], 143 | "computed-property-spacing": ["error", "never"], 144 | "consistent-this": ["error"], 145 | "eol-last": ["error"], 146 | "func-names": ["error", "always"], 147 | "func-style": ["error", "expression"], 148 | "id-blacklist": ["error", "err"], 149 | "id-length": ["error", {"min": 1, "max": 30}], 150 | "id-match": ["off", ""], 151 | "indent": ["error", 4, { "SwitchCase": 1 }], 152 | "jsx-quotes": ["error", "prefer-double"], 153 | "key-spacing": ["error"], 154 | "keyword-spacing": ["error"], 155 | "linebreak-style": ["off", "unix"], 156 | "lines-around-comment": ["off"], 157 | "max-depth": ["error", 4], 158 | "max-len": ["error", 100], 159 | "max-lines": ["error", 5000], 160 | "max-nested-callbacks": ["error", 3], 161 | "max-params": ["error", 3], 162 | "max-statements": ["error", 30], 163 | "max-statements-per-line": ["error", { "max": 1 }], 164 | "new-cap": ["error"], 165 | "new-parens": ["error"], 166 | "newline-after-var": ["off", "always"], 167 | "newline-before-return": ["off"], 168 | "newline-per-chained-call": ["error"], 169 | "no-array-constructor": ["error"], 170 | "no-bitwise": ["error"], 171 | "no-continue": ["error"], 172 | "no-inline-comments": ["error"], 173 | "no-lonely-if": ["error"], 174 | "no-mixed-operators": ["error"], 175 | "no-mixed-spaces-and-tabs": ["error"], 176 | "no-multiple-empty-lines": ["error", { "max": 2, "maxBOF": 1, "maxEOF": 1 }], 177 | "no-negated-condition": ["error"], 178 | "no-nested-ternary": ["error"], 179 | "no-new-object": ["error"], 180 | "no-plusplus": ["error"], 181 | "no-restricted-syntax": ["error", "WithStatement"], 182 | "no-spaced-func": ["error"], 183 | "no-ternary": ["off"], 184 | "no-trailing-spaces": ["error"], 185 | "no-underscore-dangle": ["error"], 186 | "no-unneeded-ternary": ["error"], 187 | "no-whitespace-before-property": ["error"], 188 | "object-curly-newline": ["error", {"multiline": true}], 189 | "object-curly-spacing": ["error", "always"], 190 | "object-property-newline": ["error"], 191 | "one-var": ["error", "always"], 192 | "one-var-declaration-per-line": ["error", "initializations"], 193 | "operator-assignment": ["error", "always"], 194 | "operator-linebreak": ["error", "after"], 195 | "padded-blocks": ["error", "never"], 196 | "quote-props": ["error", "as-needed"], 197 | "quotes": ["error", "double"], 198 | "require-jsdoc": ["error", { 199 | "require": { 200 | "FunctionDeclaration": true, 201 | "MethodDefinition": false, 202 | "ClassDeclaration": false 203 | } 204 | } 205 | ], 206 | "semi": ["error", "never"], 207 | "semi-spacing": ["error", {"before": false, "after": true}], 208 | "sort-vars": ["error"], 209 | "space-before-blocks": ["error", "always"], 210 | "space-before-function-paren": ["error", "never"], 211 | "space-in-parens": ["error", "never"], 212 | "space-infix-ops": ["error"], 213 | "space-unary-ops": ["error"], 214 | "spaced-comment": ["error"], 215 | "unicode-bom": ["error", "never"], 216 | "wrap-regex": ["error"], 217 | 218 | "arrow-body-style": ["warn", "as-needed", { "requireReturnForObjectLiteral": true }], 219 | "arrow-parens": ["error", "as-needed"], 220 | "arrow-spacing": ["error", { "before": true, "after": true }], 221 | "constructor-super": ["error"], 222 | "generator-star-spacing": ["error", {"before": true, "after": true}], 223 | "no-class-assign": ["error"], 224 | "no-confusing-arrow": ["error"], 225 | "no-const-assign": ["error"], 226 | "no-dupe-class-members": ["error"], 227 | "no-duplicate-imports": ["error"], 228 | "no-new-symbol": ["error"], 229 | "no-restricted-imports": ["error"], 230 | "no-this-before-super": ["error"], 231 | "no-useless-computed-key": ["error"], 232 | "no-useless-constructor": ["error"], 233 | "no-useless-rename": ["error"], 234 | "no-var": ["error"], 235 | "object-shorthand": ["error"], 236 | "prefer-arrow-callback": ["error"], 237 | "prefer-const": ["off"], 238 | "prefer-reflect": ["error"], 239 | "prefer-rest-params": ["error"], 240 | "prefer-spread": ["error"], 241 | "prefer-template": ["error"], 242 | "require-yield": ["error"], 243 | "rest-spread-spacing": ["error", "never"], 244 | "sort-imports": ["error", { 245 | "ignoreCase": false, 246 | "ignoreMemberSort": false, 247 | "memberSyntaxSortOrder": ["none", "all", "multiple", "single"] 248 | }], 249 | "template-curly-spacing": ["error", "never"], 250 | "yield-star-spacing": ["error", {"before": true, "after": false}] 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | coverage/** 5 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | notifications: 5 | email: false 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (MIT License) 2 | 3 | Copyright (c) 2016 Kenny Tran, Matthew Rourke, Phoebe Li, Thomas Reichling 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PurifyCSS Extended 2 | 3 | [![Travis](https://img.shields.io/travis/HapLifeMan/purifycss-extended/master.svg)]() 4 | [![Downloads](https://img.shields.io/npm/dt/purifycss-extended.svg)](https://www.npmjs.com/package/purifycss-extended) 5 | [![Realease](https://img.shields.io/npm/v/purifycss-extended.svg)](https://github.com/HapLifeMan/purifycss-extended/releases) 6 | [![License](https://img.shields.io/npm/l/tailwindcss.svg)](https://github.com/HapLifeMan/purifycss-extended/blob/master/LICENSE) 7 | 8 | ### This repo is no longer maintainer, I suggest you to user [PurgeCSS](https://github.com/FullHuman/PurgeCSS), which is better. It checks classes name instead of each word in files as PurifyCSS. 9 | 10 | 11 | **This is a fork from the original [purifycss/purifycss](https://github.com/purifycss/purifycss). 12 | Since it's not maintained for months and pull requests not merged, I decided to create a new NPM package called `purifycss-extended` based on it with some fixes.** 13 | 14 | **Everything runs as the original `purifycss` package, commands haven't changed.** 15 | 16 | A function that takes content (HTML/JS/PHP/etc) and CSS, and returns only the **used CSS**. 17 | PurifyCSS Extended does not modify the original CSS files. You can write to a new file, like minification. 18 | If your application is using a CSS framework, this is especially useful as many selectors are often unused. 19 | 20 | ### Potential reduction 21 | 22 | * [Bootstrap](https://github.com/twbs/bootstrap) file: ~140k 23 | * App using ~40% of selectors. 24 | * Minified: ~117k 25 | * Purified + Minified: **~35k** 26 | 27 | 28 | ## Usage 29 | 30 | ### Standalone 31 | 32 | Installation 33 | 34 | ```bash 35 | npm i -D purifycss-extended 36 | ``` 37 | 38 | ```javascript 39 | import purifycss from "purifycss-extended" 40 | const purifycss = require("purifycss-extended") 41 | 42 | let content = "" 43 | let css = "" 44 | let options = { 45 | output: "filepath/output.css" 46 | } 47 | purify(content, css, options) 48 | ``` 49 | 50 | ### Build Time 51 | 52 | - [Grunt](https://github.com/purifycss/grunt-purifycss) (old purifycss, open an issue if you are interested in) 53 | - [Gulp](https://github.com/purifycss/gulp-purifycss) (old purifycss, open an issue if you are interested in) 54 | - [Webpack](https://github.com/HapLifeMan/purifycss-extended-webpack) **extended special** 55 | 56 | ### CLI Usage 57 | 58 | ``` 59 | $ npm install -g purifycss-extended 60 | ``` 61 | 62 | ``` 63 | $ purifycss -h 64 | 65 | purifycss [option] 66 | 67 | Options: 68 | -m, --min Minify CSS [boolean] [default: false] 69 | -o, --out Filepath to write purified css to [string] 70 | -i, --info Logs info on how much css was removed 71 | [boolean] [default: false] 72 | -r, --rejected Logs the CSS rules that were removed 73 | [boolean] [default: false] 74 | -w, --whitelist List of classes that should not be removed 75 | [array] [default: []] 76 | -h, --help Show help [boolean] 77 | -v, --version Show version number [boolean] 78 | ``` 79 | 80 | 81 | ## How it works 82 | 83 | ### Used selector detection 84 | 85 | Statically analyzes your code to pick up which selectors are used. 86 | But will it catch all of the cases? 87 | 88 | #### Let's start off simple. 89 | #### Detecting the use of: `button-active` 90 | 91 | ``` html 92 | 93 | 94 |
click
95 | ``` 96 | 97 | ``` javascript 98 | // javascript 99 | // Anytime your class name is together in your files, it will find it. 100 | $(button).addClass('button-active'); 101 | ``` 102 | 103 | #### Now let's get crazy. 104 | #### Detecting the use of: `button-active` 105 | 106 | ``` javascript 107 | // Can detect if class is split. 108 | var half = 'button-'; 109 | $(button).addClass(half + 'active'); 110 | 111 | // Can detect if class is joined. 112 | var dynamicClass = ['button', 'active'].join('-'); 113 | $(button).addClass(dynamicClass); 114 | 115 | // Can detect various more ways, including all Javascript frameworks. 116 | // A React example. 117 | var classes = classNames({ 118 | 'button-active': this.state.buttonActive 119 | }); 120 | 121 | return ( 122 | ; 123 | ); 124 | ``` 125 | 126 | ### Examples 127 | 128 | 129 | ##### Example with source strings 130 | 131 | ```js 132 | var content = ''; 133 | var css = '.button-active { color: green; } .unused-class { display: block; }'; 134 | 135 | console.log(purify(content, css)); 136 | ``` 137 | 138 | logs out: 139 | 140 | ``` 141 | .button-active { color: green; } 142 | ``` 143 | 144 | 145 | ##### Example with [glob](https://github.com/isaacs/node-glob) file patterns + writing to a file 146 | 147 | ```js 148 | var content = ['**/src/js/*.js', '**/src/html/*.html']; 149 | var css = ['**/src/css/*.css']; 150 | 151 | var options = { 152 | // Will write purified CSS to this file. 153 | output: './dist/purified.css' 154 | }; 155 | 156 | purify(content, css, options); 157 | ``` 158 | 159 | 160 | ##### Example with both [glob](https://github.com/isaacs/node-glob) file patterns and source strings + minify + logging rejected selectors 161 | 162 | ```js 163 | var content = ['**/src/js/*.js', '**/src/html/*.html']; 164 | var css = '.button-active { color: green; } .unused-class { display: block; }'; 165 | 166 | var options = { 167 | output: './dist/purified.css', 168 | 169 | // Will minify CSS code in addition to purify. 170 | minify: true, 171 | 172 | // Logs out removed selectors. 173 | rejected: true 174 | }; 175 | 176 | purify(content, css, options); 177 | ``` 178 | logs out: 179 | 180 | ``` 181 | .unused-class 182 | ``` 183 | 184 | 185 | ##### Example with callback 186 | 187 | ```js 188 | var content = ['**/src/js/*.js', '**/src/html/*.html']; 189 | var css = ['**/src/css/*.css']; 190 | 191 | purify(content, css, function (purifiedResult) { 192 | console.log(purifiedResult); 193 | }); 194 | ``` 195 | 196 | 197 | ##### Example with callback + options 198 | 199 | ```js 200 | var content = ['**/src/js/*.js', '**/src/html/*.html']; 201 | var css = ['**/src/css/*.css']; 202 | 203 | var options = { 204 | minify: true 205 | }; 206 | 207 | purify(content, css, options, function (purifiedAndMinifiedResult) { 208 | console.log(purifiedAndMinifiedResult); 209 | }); 210 | ``` 211 | 212 | ### API in depth 213 | 214 | ```javascript 215 | // Four possible arguments. 216 | purify(content, css, options, callback); 217 | ``` 218 | 219 | ##### The `content` argument 220 | ##### Type: `Array` or `String` 221 | 222 | **`Array`** of [glob](https://github.com/isaacs/node-glob) file patterns to the files to search through for used classes (HTML, JS, PHP, ERB, Templates, anything that uses CSS selectors). 223 | 224 | **`String`** of content to look at for used classes. 225 | 226 |
227 | 228 | ##### The `css` argument 229 | ##### Type: `Array` or `String` 230 | 231 | **`Array`** of [glob](https://github.com/isaacs/node-glob) file patterns to the CSS files you want to filter. 232 | 233 | **`String`** of CSS to purify. 234 | 235 |
236 | 237 | ##### The (optional) `options` argument 238 | ##### Type: `Object` 239 | 240 | ##### Properties of options object: 241 | 242 | * **`minify:`** Set to `true` to minify. Default: `false`. 243 | 244 | * **`output:`** Filepath to write purified CSS to. Returns raw string if `false`. Default: `false`. 245 | 246 | * **`info:`** Logs info on how much CSS was removed if `true`. Default: `false`. 247 | 248 | * **`rejected:`** Logs the CSS rules that were removed if `true`. Default: `false`. 249 | 250 | * **`whitelist`** Array of selectors to always leave in. Ex. `['button-active', '*modal*']` this will leave any selector that includes `modal` in it and selectors that match `button-active`. (wrapping the string with *'s, leaves all selectors that include it) 251 | 252 | 253 | 254 | ##### The (optional) ```callback``` argument 255 | ##### Type: `Function` 256 | 257 | A function that will receive the purified CSS as it's argument. 258 | 259 | ##### Example of callback use 260 | ``` javascript 261 | purify(content, css, options, function(purifiedCSS){ 262 | console.log(purifiedCSS, ' is the result of purify'); 263 | }); 264 | ``` 265 | 266 | ##### Example of callback without options 267 | ``` javascript 268 | purify(content, css, function(purifiedCSS){ 269 | console.log('callback without options and received', purifiedCSS); 270 | }); 271 | ``` 272 | 273 | ##### Example CLI Usage 274 | 275 | ``` 276 | $ purifycss src/css/main.css src/css/bootstrap.css src/js/main.js --min --info --out src/dist/index.css 277 | ``` 278 | This will concat both `main.css` and `bootstrap.css` and purify it by looking at what CSS selectors were used inside of `main.js`. It will then write the result to `dist/index.css` 279 | 280 | The `--min` flag minifies the result. 281 | 282 | The `--info` flag will print this to stdout: 283 | ``` 284 | ________________________________________________ 285 | | 286 | | PurifyCSS has reduced the file size by ~ 33.8% 287 | | 288 | ________________________________________________ 289 | 290 | ``` 291 | The CLI currently does not support file patterns. 292 | -------------------------------------------------------------------------------- /__tests__/index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const purify = require("../lib/purifycss.js") 3 | const absPath = `${__dirname}/test_examples/` 4 | const read = path => fs.readFileSync(absPath + path, "utf8") 5 | 6 | describe("find intact classes", () => { 7 | const content = read("simple/simple.js"), 8 | css = read("simple/simple.css"), 9 | result = purify(content, css) 10 | 11 | it("finds .single", () => { 12 | expect(result.includes(".single") === true).toBe(true) 13 | }) 14 | 15 | it("finds .double-class", () => { 16 | expect(result.includes(".double-class") === true).toBe(true) 17 | }) 18 | 19 | it("can find .triple-simple-class", () => { 20 | expect(result.includes(".triple-simple-class") === true).toBe(true) 21 | }) 22 | }) 23 | 24 | describe("callback", () => { 25 | const content = read("simple/simple.js"), 26 | css = read("simple/simple.css") 27 | 28 | it("can use a callback", () => { 29 | purify(content, css, result => { 30 | expect(result.includes(".triple-simple-class") === true).toBe(true) 31 | }) 32 | }) 33 | }) 34 | 35 | describe("classes that are added together", () => { 36 | const content = read("combined/combined.js"), 37 | css = read("combined/combined.css"), 38 | result = purify(content, css) 39 | 40 | it("can find .added-together", () => { 41 | expect(result.includes(".added-together") === true).toBe(true) 42 | }) 43 | 44 | it("can find .array-joined", () => { 45 | expect(result.includes(".array-joined") === true).toBe(true) 46 | }) 47 | }) 48 | 49 | describe("filters out unused selectors", () => { 50 | const content = read("remove_unused/remove_unused.js"), 51 | css = read("remove_unused/remove_unused.css"), 52 | result = purify(content, css) 53 | 54 | it("contains .used-class", () => { 55 | expect(result.includes(".used-class") === true).toBe(true) 56 | }) 57 | 58 | it("removes .unused-class", () => { 59 | expect(result.includes(".unused-class") === false).toBe(true) 60 | }) 61 | 62 | it("removes .another-one-not-found", () => { 63 | expect(result.includes(".another-one-not-found") === false).toBe(true) 64 | }) 65 | }) 66 | 67 | describe("works with multiple files", () => { 68 | const content = ["**/test_examples/multiple_files/*.+(js|html)"], 69 | css = ["**/test_examples/multiple_files/*.css"], 70 | result = purify(content, css) 71 | 72 | it("finds .taylor-swift", () => { 73 | expect(result.includes(".taylor-swift") === true).toBe(true) 74 | }) 75 | 76 | it("finds .blank-space", () => { 77 | expect(result.includes(".blank-space") === true).toBe(true) 78 | }) 79 | 80 | it("removes .shake-it-off", () => { 81 | expect(result.includes(".shake-it-off") === false).toBe(true) 82 | }) 83 | }) 84 | 85 | describe("camelCase", () => { 86 | const content = read("camel_case/camel_case.js"), 87 | css = read("camel_case/camel_case.css"), 88 | result = purify(content, css) 89 | 90 | it("finds testFoo", () => { 91 | expect(result.includes("testFoo") === true).toBe(true) 92 | }) 93 | 94 | it("finds camelCase", () => { 95 | expect(result.includes("camelCase") === true).toBe(true) 96 | }) 97 | }) 98 | 99 | describe("wildcard", () => { 100 | const content = read("wildcard/wildcard.html"), 101 | css = read("wildcard/wildcard.css"), 102 | result = purify(content, css) 103 | 104 | it("finds universal selector", () => { 105 | expect(result.includes("*") === true).toBe(true) 106 | }) 107 | 108 | it("finds :before", () => { 109 | expect(result.includes("before") === true).toBe(true) 110 | }) 111 | 112 | it("finds scrollbar", () => { 113 | expect(result.includes("scrollbar") === true).toBe(true) 114 | }) 115 | 116 | it("finds selection", () => { 117 | expect(result.includes("selection") === true).toBe(true) 118 | }) 119 | 120 | it("finds vertical", () => { 121 | expect(result.includes("vertical") === true).toBe(true) 122 | }) 123 | 124 | it("finds :root", () => { 125 | expect(result.includes(":root") === true).toBe(true) 126 | }) 127 | }) 128 | 129 | describe("media queries", () => { 130 | const content = read("media_queries/media_queries.html"), 131 | css = read("media_queries/media_queries.css"), 132 | result = purify(content, css) 133 | 134 | it("finds .media-class", () => { 135 | expect(result.includes(".media-class") === true).toBe(true) 136 | }) 137 | 138 | it("finds .alone", () => { 139 | expect(result.includes(".alone") === true).toBe(true) 140 | }) 141 | 142 | it("finds #id-in-media", () => { 143 | expect(result.includes("#id-in-media") === true).toBe(true) 144 | }) 145 | 146 | it("finds body", () => { 147 | expect(result.includes("body") === true).toBe(true) 148 | }) 149 | 150 | it("removes .unused-class", () => { 151 | expect(result.includes(".unused-class") === true).toBe(false) 152 | }) 153 | 154 | it("removes the empty media query", () => { 155 | expect(result.includes("66666px") === true).toBe(false) 156 | }) 157 | }) 158 | 159 | describe("attribute selectors", () => { 160 | const content = read("attribute_selector/attribute_selector.html"), 161 | css = read("attribute_selector/attribute_selector.css"), 162 | result = purify(content, css) 163 | 164 | it("finds font-icon-", () => { 165 | expect(result.includes("font-icon-") === true).toBe(true) 166 | }) 167 | 168 | it("finds center aligned", () => { 169 | expect(result.includes("center aligned") === true).toBe(true) 170 | }) 171 | 172 | it("does not find github", () => { 173 | expect(result.includes("github") === true).toBe(false) 174 | }) 175 | }) 176 | 177 | describe("special characters", () => { 178 | const content = read("special_characters/special_characters.js"), 179 | css = read("special_characters/special_characters.css"), 180 | result = purify(content, css) 181 | 182 | it("finds @home", () => { 183 | expect(result.includes(".\\@home") === true).toBe(true) 184 | }) 185 | 186 | it("finds +rounded", () => { 187 | expect(result.includes(".\\+rounded") === true).toBe(true) 188 | }) 189 | 190 | it("finds button", () => { 191 | expect(result.includes(".button") === true).toBe(true) 192 | }) 193 | 194 | it("finds button@home", () => { 195 | expect(result.includes(".button\\@home") === true).toBe(true) 196 | }) 197 | }) 198 | 199 | describe("delimited", () => { 200 | const content = read("delimited/delimited.html"), 201 | css = read("delimited/delimited.css"), 202 | result = purify(content, css) 203 | 204 | it("removes the extra comma", () => { 205 | var commaCount = result.split("").reduce( 206 | (total, chr) => { 207 | if (chr === ",") { 208 | return total + 1 209 | } 210 | 211 | return total 212 | }, 213 | 0 214 | ) 215 | 216 | expect(commaCount).toBe(1) 217 | }) 218 | 219 | it("finds h1", () => { 220 | expect(result.includes("h1") === true).toBe(true) 221 | }) 222 | 223 | it("finds p", () => { 224 | expect(result.includes("p") === true).toBe(true) 225 | }) 226 | 227 | it("removes .unused-class-name", () => { 228 | expect(result.includes(".unused-class-name") === false).toBe(true) 229 | }) 230 | }) 231 | /* 232 | describe("removal of selectors", () => { 233 | beforeEach(() => { 234 | this.css = read("bootstrap/modified-bootstrap.css") 235 | }) 236 | 237 | it("should only have .testFoo", () => { 238 | var css = this.css + read("camel_case/camel_case.css") 239 | var result = purify("testfoo", css) 240 | 241 | expect(result.length < 400).toBe(true) 242 | expect(result.includes(".testFoo") === true).toBe(true) 243 | }) 244 | }) 245 | */ 246 | describe("pseudo classes", () => { 247 | const content = read("pseudo_class/pseudo_class.js"), 248 | css = read("pseudo_class/pseudo_class.css"), 249 | result = purify(content, css) 250 | 251 | it("finds div:before", () => { 252 | expect(result.includes("div:before") === true).toBe(true) 253 | }) 254 | 255 | it("removes row:after", () => { 256 | expect(result.includes("row:after") === true).toBe(false) 257 | }) 258 | }) 259 | 260 | describe("special", () => { 261 | const content = read("special/special.js"), 262 | css = read("special/special.css"), 263 | result = purify(content, css) 264 | 265 | it("finds mr-10 (alone)", () => { 266 | expect(result.includes(".mr-10") === true).toBe(true) 267 | }) 268 | 269 | it("finds -mr-10 (alone)", () => { 270 | expect(result.includes(".-mr-10") === true).toBe(true) 271 | }) 272 | 273 | it("finds lg:mr-10 (alone)", () => { 274 | expect(result.includes(".lg\\:mr-10") === true).toBe(true) 275 | }) 276 | 277 | it("finds lg:-mr-10 (alone)", () => { 278 | expect(result.includes(".lg\\:-mr-10") === true).toBe(true) 279 | }) 280 | 281 | it("finds tw-sm:mr-10 (alone)", () => { 282 | expect(result.includes(".tw-sm\\:mr-10") === true).toBe(true) 283 | }) 284 | 285 | it("finds tw-sm:-mr-10 (alone)", () => { 286 | expect(result.includes(".tw-sm\\:-mr-10") === true).toBe(true) 287 | }) 288 | 289 | it("finds mr-20 (surrounded)", () => { 290 | expect(result.includes(".mr-20") === true).toBe(true) 291 | }) 292 | 293 | it("finds -mr-20 (surrounded)", () => { 294 | expect(result.includes(".-mr-20") === true).toBe(true) 295 | }) 296 | 297 | it("finds lg:mr-20 (surrounded)", () => { 298 | expect(result.includes(".lg\\:mr-20") === true).toBe(true) 299 | }) 300 | 301 | it("finds lg:-mr-20 (surrounded)", () => { 302 | expect(result.includes(".lg\\:-mr-20") === true).toBe(true) 303 | }) 304 | 305 | it("finds tw-sm:mr-20 (surrounded)", () => { 306 | expect(result.includes(".tw-sm\\:mr-20") === true).toBe(true) 307 | }) 308 | 309 | it("finds tw-sm:-mr-20 (surrounded)", () => { 310 | expect(result.includes(".tw-sm\\:-mr-20") === true).toBe(true) 311 | }) 312 | 313 | it("removes mr-50", () => { 314 | expect(result.includes(".mr-50") === true).toBe(false) 315 | }) 316 | 317 | it("removes -mr-50", () => { 318 | expect(result.includes(".-mr-50") === true).toBe(false) 319 | }) 320 | 321 | it("removes lg:mr-50", () => { 322 | expect(result.includes(".lg\\:mr-50") === true).toBe(false) 323 | }) 324 | 325 | it("removes lg:-mr-50", () => { 326 | expect(result.includes(".lg\\:-mr-50") === true).toBe(false) 327 | }) 328 | 329 | it("removes tw-sm:mr-50", () => { 330 | expect(result.includes(".tw-sm\\:mr-50") === true).toBe(false) 331 | }) 332 | 333 | it("removes tw-sm:-mr-50", () => { 334 | expect(result.includes(".tw-sm\\:-mr-50") === true).toBe(false) 335 | }) 336 | 337 | }) 338 | -------------------------------------------------------------------------------- /__tests__/test_examples/attribute_selector/attribute_selector.css: -------------------------------------------------------------------------------- 1 | 2 | [class*="font-icon-"] { 3 | color: black; 4 | } 5 | 6 | a.link[href*="github.com"] { 7 | color: black; 8 | } 9 | 10 | .ui[class*="center aligned"].grid { 11 | color: black; 12 | } 13 | -------------------------------------------------------------------------------- /__tests__/test_examples/attribute_selector/attribute_selector.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | -------------------------------------------------------------------------------- /__tests__/test_examples/camel_case/camel_case.css: -------------------------------------------------------------------------------- 1 | .testFoo { 2 | color: black; 3 | } 4 | 5 | #camelCase { 6 | color: black; 7 | } -------------------------------------------------------------------------------- /__tests__/test_examples/camel_case/camel_case.js: -------------------------------------------------------------------------------- 1 | 'testFoo' 2 | 3 | 'camelCase' 4 | -------------------------------------------------------------------------------- /__tests__/test_examples/combined/combined.css: -------------------------------------------------------------------------------- 1 | .added-together { 2 | color: black; 3 | } 4 | 5 | .array-joined { 6 | color: black; 7 | } 8 | -------------------------------------------------------------------------------- /__tests__/test_examples/combined/combined.js: -------------------------------------------------------------------------------- 1 | var x = 'added-' + 'together'; 2 | 3 | var y = ['array', 'joined'].join('-'); 4 | -------------------------------------------------------------------------------- /__tests__/test_examples/delimited/delimited.css: -------------------------------------------------------------------------------- 1 | .unused-class-name, h1, p { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /__tests__/test_examples/delimited/delimited.html: -------------------------------------------------------------------------------- 1 |

hello

2 |

world

3 | -------------------------------------------------------------------------------- /__tests__/test_examples/media_queries/media_queries.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 600px) { 2 | div.media-class { 3 | color: black; 4 | } 5 | 6 | .alone, .unused-class { 7 | color: black; 8 | } 9 | 10 | #id-in-media { 11 | color: black; 12 | } 13 | 14 | body { 15 | color: black; 16 | } 17 | } 18 | 19 | @media (max-width: 960px){ 20 | .alone, .unused-class { 21 | color: black; 22 | } 23 | *, 24 | :before, 25 | :after { 26 | background: black; 27 | } 28 | } 29 | 30 | @media (max-width: 66666px){ 31 | .unused-class, .unused-class2 { 32 | color: black; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /__tests__/test_examples/media_queries/media_queries.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 'alone' 4 | 5 | 'id-in-media' 6 | 7 | 8 | -------------------------------------------------------------------------------- /__tests__/test_examples/multiple_files/multiple_files.css: -------------------------------------------------------------------------------- 1 | .blank-space { 2 | color: black; 3 | } 4 | -------------------------------------------------------------------------------- /__tests__/test_examples/multiple_files/multiple_files.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /__tests__/test_examples/multiple_files/multiple_files.js: -------------------------------------------------------------------------------- 1 | 'blank-space' 2 | -------------------------------------------------------------------------------- /__tests__/test_examples/multiple_files/multiple_files2.css: -------------------------------------------------------------------------------- 1 | .taylor-swift { 2 | color: black; 3 | } 4 | 5 | .shake-it-off { 6 | color: black; 7 | } 8 | -------------------------------------------------------------------------------- /__tests__/test_examples/pseudo_class/pseudo_class.css: -------------------------------------------------------------------------------- 1 | div:before { 2 | color: black; 3 | } 4 | 5 | .row:after { 6 | color: black; 7 | } 8 | -------------------------------------------------------------------------------- /__tests__/test_examples/pseudo_class/pseudo_class.js: -------------------------------------------------------------------------------- 1 | 'div' 2 | -------------------------------------------------------------------------------- /__tests__/test_examples/remove_unused/remove_unused.css: -------------------------------------------------------------------------------- 1 | .used-class { 2 | color: black; 3 | } 4 | 5 | .unused-class { 6 | color: black; 7 | } 8 | 9 | .another-one-not-found { 10 | color: black; 11 | } 12 | -------------------------------------------------------------------------------- /__tests__/test_examples/remove_unused/remove_unused.js: -------------------------------------------------------------------------------- 1 | '.used-class' 2 | -------------------------------------------------------------------------------- /__tests__/test_examples/simple/simple.css: -------------------------------------------------------------------------------- 1 | .single { 2 | color: black; 3 | } 4 | 5 | .double-class { 6 | color: black; 7 | } 8 | 9 | .triple-simple-class { 10 | color: black; 11 | } 12 | -------------------------------------------------------------------------------- /__tests__/test_examples/simple/simple.js: -------------------------------------------------------------------------------- 1 | 'single' 2 | 3 | 'double-class' 4 | 5 | 'triple-simple-class' 6 | -------------------------------------------------------------------------------- /__tests__/test_examples/special/special.css: -------------------------------------------------------------------------------- 1 | /* Existant CSS in file (alone) */ 2 | .mr-10 { color: dodgerblue; } 3 | .-mr-10 { color: dodgerblue; } 4 | .lg\:mr-10 { color: white; } 5 | .lg\:-mr-10 { color: white; } 6 | .tw-sm\:mr-10 { color: crimson; } 7 | .tw-sm\:-mr-10 { color: crimson; } 8 | 9 | /* Existant CSS in file (surrounded by other classes) */ 10 | .mr-20 { color: dodgerblue; } 11 | .-mr-20 { color: dodgerblue; } 12 | .lg\:mr-20 { color: white; } 13 | .lg\:-mr-20 { color: white; } 14 | .tw-sm\:mr-20 { color: crimson; } 15 | .tw-sm\:-mr-20 { color: crimson; } 16 | 17 | /* Inexistant CSS in file */ 18 | .mr-50 { color: dodgerblue; } 19 | .-mr-50 { color: dodgerblue; } 20 | .lg\:mr-50 { color: white; } 21 | .lg\:-mr-50 { color: white; } 22 | .tw-sm\:mr-50 { color: crimson; } 23 | .tw-sm\:-mr-50 { color: crimson; } -------------------------------------------------------------------------------- /__tests__/test_examples/special/special.js: -------------------------------------------------------------------------------- 1 | 'mr-10' 2 | 3 | 'btn mr-20 btn-black' 4 | 5 | '-mr-10' 6 | 7 | 'btn -mr-20 btn-black' 8 | 9 | 'lg:mr-10' 10 | 11 | 'btn lg:mr-20 btn-black' 12 | 13 | 'lg:-mr-10' 14 | 15 | 'btn lg:-mr-20 btn-black' 16 | 17 | 'tw-sm:mr-10' 18 | 19 | 'btn tw-sm:mr-20 btn-black' 20 | 21 | 'tw-sm:-mr-10' 22 | 23 | 'btn tw-sm:-mr-20 btn-black' -------------------------------------------------------------------------------- /__tests__/test_examples/special_characters/special_characters.css: -------------------------------------------------------------------------------- 1 | .\@home { 2 | color: black; 3 | } 4 | 5 | .button.\+rounded { 6 | color: black; 7 | } 8 | 9 | .button\@home { 10 | color: black; 11 | } -------------------------------------------------------------------------------- /__tests__/test_examples/special_characters/special_characters.js: -------------------------------------------------------------------------------- 1 | '@home' 2 | 3 | '+rounded' 4 | 5 | 'button' 6 | 7 | 'button@home' -------------------------------------------------------------------------------- /__tests__/test_examples/wildcard/wildcard.css: -------------------------------------------------------------------------------- 1 | *, 2 | ::before, 3 | ::after { 4 | background: black; 5 | } 6 | 7 | :root { 8 | background: black; 9 | } 10 | 11 | ::selection { 12 | background: black; 13 | } 14 | 15 | ::-webkit-scrollbar { 16 | background: black; 17 | } 18 | 19 | ::-webkit-scrollbar:vertical { 20 | background: black; 21 | } 22 | -------------------------------------------------------------------------------- /__tests__/test_examples/wildcard/wildcard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | wildcard 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /bin/purifycss: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var yargs = require("yargs") 3 | var path = require("path") 4 | var purify = require("../lib/purifycss.js") 5 | 6 | var argv = yargs 7 | .usage("$0 [option]") 8 | .option("m", { 9 | alias: "min", 10 | description: "Minify CSS", 11 | type: "boolean", 12 | default: false 13 | }) 14 | .option("o", { 15 | alias: "out", 16 | description: "Filepath to write purified css to", 17 | type: "string" 18 | }) 19 | .option("i", { 20 | alias: "info", 21 | description: "Logs info on how much css was removed", 22 | type: "boolean", 23 | default: false 24 | }) 25 | .option("r", { 26 | alias: "rejected", 27 | description: "Logs the CSS rules that were removed", 28 | type: "boolean", 29 | default: false 30 | }) 31 | .option("w", { 32 | alias: "whitelist", 33 | description: "List of classes that should not be removed", 34 | type: "array", 35 | default: [] 36 | }) 37 | .demandCommand(2) 38 | .help() 39 | .alias("h", "help") 40 | .version() 41 | .alias("v", "version") 42 | .argv 43 | 44 | var files = argv._ 45 | var css = files.filter(function (file) { 46 | return path.extname(file) === ".css" 47 | }) 48 | var content = files.filter(function (file) { 49 | return path.extname(file) !== ".css" 50 | }) 51 | 52 | var options = { 53 | minify: argv.min, 54 | info: argv.info, 55 | rejected: argv.rejected, 56 | output: argv.out, 57 | whitelist: argv.whitelist 58 | } 59 | 60 | if (options.output) { 61 | purify(content, css, options) 62 | } else { 63 | console.log(purify(content, css, options)) 64 | } 65 | -------------------------------------------------------------------------------- /config/rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from "rollup-plugin-babel" 2 | import builtins from "rollup-plugin-node-builtins" 3 | import commonjs from "rollup-plugin-commonjs" 4 | import resolve from "rollup-plugin-node-resolve" 5 | 6 | export default { 7 | entry: "src/purifycss.js", 8 | targets: [ 9 | { 10 | dest: "lib/purifycss.es.js", 11 | format: "es" 12 | }, 13 | { 14 | dest: "lib/purifycss.js", 15 | format: "cjs" 16 | } 17 | ], 18 | plugins: [ 19 | builtins(), 20 | resolve(), 21 | commonjs(), 22 | babel({ 23 | exclude: "node_modules/**", 24 | presets: [ 25 | [ 26 | "es2015", { 27 | "modules": false 28 | } 29 | ] 30 | ], 31 | "plugins": [ "external-helpers" ] 32 | }) 33 | ], 34 | external: ["clean-css", "glob", "rework", "uglifyjs"], 35 | sourceMap: false 36 | } 37 | -------------------------------------------------------------------------------- /lib/purifycss.es.js: -------------------------------------------------------------------------------- 1 | import CleanCss from 'clean-css'; 2 | import rework from 'rework'; 3 | import glob from 'glob'; 4 | 5 | var domain; 6 | 7 | // This constructor is used to store event handlers. Instantiating this is 8 | // faster than explicitly calling `Object.create(null)` to get a "clean" empty 9 | // object (tested with v8 v4.9). 10 | function EventHandlers() {} 11 | EventHandlers.prototype = Object.create(null); 12 | 13 | function EventEmitter() { 14 | EventEmitter.init.call(this); 15 | } 16 | // nodejs oddity 17 | // require('events') === require('events').EventEmitter 18 | EventEmitter.EventEmitter = EventEmitter; 19 | 20 | EventEmitter.usingDomains = false; 21 | 22 | EventEmitter.prototype.domain = undefined; 23 | EventEmitter.prototype._events = undefined; 24 | EventEmitter.prototype._maxListeners = undefined; 25 | 26 | // By default EventEmitters will print a warning if more than 10 listeners are 27 | // added to it. This is a useful default which helps finding memory leaks. 28 | EventEmitter.defaultMaxListeners = 10; 29 | 30 | EventEmitter.init = function() { 31 | this.domain = null; 32 | if (EventEmitter.usingDomains) { 33 | // if there is an active domain, then attach to it. 34 | if (domain.active && !(this instanceof domain.Domain)) { 35 | this.domain = domain.active; 36 | } 37 | } 38 | 39 | if (!this._events || this._events === Object.getPrototypeOf(this)._events) { 40 | this._events = new EventHandlers(); 41 | this._eventsCount = 0; 42 | } 43 | 44 | this._maxListeners = this._maxListeners || undefined; 45 | }; 46 | 47 | // Obviously not all Emitters should be limited to 10. This function allows 48 | // that to be increased. Set to zero for unlimited. 49 | EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { 50 | if (typeof n !== 'number' || n < 0 || isNaN(n)) 51 | throw new TypeError('"n" argument must be a positive number'); 52 | this._maxListeners = n; 53 | return this; 54 | }; 55 | 56 | function $getMaxListeners(that) { 57 | if (that._maxListeners === undefined) 58 | return EventEmitter.defaultMaxListeners; 59 | return that._maxListeners; 60 | } 61 | 62 | EventEmitter.prototype.getMaxListeners = function getMaxListeners() { 63 | return $getMaxListeners(this); 64 | }; 65 | 66 | // These standalone emit* functions are used to optimize calling of event 67 | // handlers for fast cases because emit() itself often has a variable number of 68 | // arguments and can be deoptimized because of that. These functions always have 69 | // the same number of arguments and thus do not get deoptimized, so the code 70 | // inside them can execute faster. 71 | function emitNone(handler, isFn, self) { 72 | if (isFn) 73 | handler.call(self); 74 | else { 75 | var len = handler.length; 76 | var listeners = arrayClone(handler, len); 77 | for (var i = 0; i < len; ++i) 78 | listeners[i].call(self); 79 | } 80 | } 81 | function emitOne(handler, isFn, self, arg1) { 82 | if (isFn) 83 | handler.call(self, arg1); 84 | else { 85 | var len = handler.length; 86 | var listeners = arrayClone(handler, len); 87 | for (var i = 0; i < len; ++i) 88 | listeners[i].call(self, arg1); 89 | } 90 | } 91 | function emitTwo(handler, isFn, self, arg1, arg2) { 92 | if (isFn) 93 | handler.call(self, arg1, arg2); 94 | else { 95 | var len = handler.length; 96 | var listeners = arrayClone(handler, len); 97 | for (var i = 0; i < len; ++i) 98 | listeners[i].call(self, arg1, arg2); 99 | } 100 | } 101 | function emitThree(handler, isFn, self, arg1, arg2, arg3) { 102 | if (isFn) 103 | handler.call(self, arg1, arg2, arg3); 104 | else { 105 | var len = handler.length; 106 | var listeners = arrayClone(handler, len); 107 | for (var i = 0; i < len; ++i) 108 | listeners[i].call(self, arg1, arg2, arg3); 109 | } 110 | } 111 | 112 | function emitMany(handler, isFn, self, args) { 113 | if (isFn) 114 | handler.apply(self, args); 115 | else { 116 | var len = handler.length; 117 | var listeners = arrayClone(handler, len); 118 | for (var i = 0; i < len; ++i) 119 | listeners[i].apply(self, args); 120 | } 121 | } 122 | 123 | EventEmitter.prototype.emit = function emit(type) { 124 | var er, handler, len, args, i, events, domain; 125 | var needDomainExit = false; 126 | var doError = (type === 'error'); 127 | 128 | events = this._events; 129 | if (events) 130 | doError = (doError && events.error == null); 131 | else if (!doError) 132 | return false; 133 | 134 | domain = this.domain; 135 | 136 | // If there is no 'error' event listener then throw. 137 | if (doError) { 138 | er = arguments[1]; 139 | if (domain) { 140 | if (!er) 141 | er = new Error('Uncaught, unspecified "error" event'); 142 | er.domainEmitter = this; 143 | er.domain = domain; 144 | er.domainThrown = false; 145 | domain.emit('error', er); 146 | } else if (er instanceof Error) { 147 | throw er; // Unhandled 'error' event 148 | } else { 149 | // At least give some kind of context to the user 150 | var err = new Error('Uncaught, unspecified "error" event. (' + er + ')'); 151 | err.context = er; 152 | throw err; 153 | } 154 | return false; 155 | } 156 | 157 | handler = events[type]; 158 | 159 | if (!handler) 160 | return false; 161 | 162 | var isFn = typeof handler === 'function'; 163 | len = arguments.length; 164 | switch (len) { 165 | // fast cases 166 | case 1: 167 | emitNone(handler, isFn, this); 168 | break; 169 | case 2: 170 | emitOne(handler, isFn, this, arguments[1]); 171 | break; 172 | case 3: 173 | emitTwo(handler, isFn, this, arguments[1], arguments[2]); 174 | break; 175 | case 4: 176 | emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]); 177 | break; 178 | // slower 179 | default: 180 | args = new Array(len - 1); 181 | for (i = 1; i < len; i++) 182 | args[i - 1] = arguments[i]; 183 | emitMany(handler, isFn, this, args); 184 | } 185 | 186 | if (needDomainExit) 187 | domain.exit(); 188 | 189 | return true; 190 | }; 191 | 192 | function _addListener(target, type, listener, prepend) { 193 | var m; 194 | var events; 195 | var existing; 196 | 197 | if (typeof listener !== 'function') 198 | throw new TypeError('"listener" argument must be a function'); 199 | 200 | events = target._events; 201 | if (!events) { 202 | events = target._events = new EventHandlers(); 203 | target._eventsCount = 0; 204 | } else { 205 | // To avoid recursion in the case that type === "newListener"! Before 206 | // adding it to the listeners, first emit "newListener". 207 | if (events.newListener) { 208 | target.emit('newListener', type, 209 | listener.listener ? listener.listener : listener); 210 | 211 | // Re-assign `events` because a newListener handler could have caused the 212 | // this._events to be assigned to a new object 213 | events = target._events; 214 | } 215 | existing = events[type]; 216 | } 217 | 218 | if (!existing) { 219 | // Optimize the case of one listener. Don't need the extra array object. 220 | existing = events[type] = listener; 221 | ++target._eventsCount; 222 | } else { 223 | if (typeof existing === 'function') { 224 | // Adding the second element, need to change to array. 225 | existing = events[type] = prepend ? [listener, existing] : 226 | [existing, listener]; 227 | } else { 228 | // If we've already got an array, just append. 229 | if (prepend) { 230 | existing.unshift(listener); 231 | } else { 232 | existing.push(listener); 233 | } 234 | } 235 | 236 | // Check for listener leak 237 | if (!existing.warned) { 238 | m = $getMaxListeners(target); 239 | if (m && m > 0 && existing.length > m) { 240 | existing.warned = true; 241 | var w = new Error('Possible EventEmitter memory leak detected. ' + 242 | existing.length + ' ' + type + ' listeners added. ' + 243 | 'Use emitter.setMaxListeners() to increase limit'); 244 | w.name = 'MaxListenersExceededWarning'; 245 | w.emitter = target; 246 | w.type = type; 247 | w.count = existing.length; 248 | emitWarning(w); 249 | } 250 | } 251 | } 252 | 253 | return target; 254 | } 255 | function emitWarning(e) { 256 | typeof console.warn === 'function' ? console.warn(e) : console.log(e); 257 | } 258 | EventEmitter.prototype.addListener = function addListener(type, listener) { 259 | return _addListener(this, type, listener, false); 260 | }; 261 | 262 | EventEmitter.prototype.on = EventEmitter.prototype.addListener; 263 | 264 | EventEmitter.prototype.prependListener = 265 | function prependListener(type, listener) { 266 | return _addListener(this, type, listener, true); 267 | }; 268 | 269 | function _onceWrap(target, type, listener) { 270 | var fired = false; 271 | function g() { 272 | target.removeListener(type, g); 273 | if (!fired) { 274 | fired = true; 275 | listener.apply(target, arguments); 276 | } 277 | } 278 | g.listener = listener; 279 | return g; 280 | } 281 | 282 | EventEmitter.prototype.once = function once(type, listener) { 283 | if (typeof listener !== 'function') 284 | throw new TypeError('"listener" argument must be a function'); 285 | this.on(type, _onceWrap(this, type, listener)); 286 | return this; 287 | }; 288 | 289 | EventEmitter.prototype.prependOnceListener = 290 | function prependOnceListener(type, listener) { 291 | if (typeof listener !== 'function') 292 | throw new TypeError('"listener" argument must be a function'); 293 | this.prependListener(type, _onceWrap(this, type, listener)); 294 | return this; 295 | }; 296 | 297 | // emits a 'removeListener' event iff the listener was removed 298 | EventEmitter.prototype.removeListener = 299 | function removeListener(type, listener) { 300 | var list, events, position, i, originalListener; 301 | 302 | if (typeof listener !== 'function') 303 | throw new TypeError('"listener" argument must be a function'); 304 | 305 | events = this._events; 306 | if (!events) 307 | return this; 308 | 309 | list = events[type]; 310 | if (!list) 311 | return this; 312 | 313 | if (list === listener || (list.listener && list.listener === listener)) { 314 | if (--this._eventsCount === 0) 315 | this._events = new EventHandlers(); 316 | else { 317 | delete events[type]; 318 | if (events.removeListener) 319 | this.emit('removeListener', type, list.listener || listener); 320 | } 321 | } else if (typeof list !== 'function') { 322 | position = -1; 323 | 324 | for (i = list.length; i-- > 0;) { 325 | if (list[i] === listener || 326 | (list[i].listener && list[i].listener === listener)) { 327 | originalListener = list[i].listener; 328 | position = i; 329 | break; 330 | } 331 | } 332 | 333 | if (position < 0) 334 | return this; 335 | 336 | if (list.length === 1) { 337 | list[0] = undefined; 338 | if (--this._eventsCount === 0) { 339 | this._events = new EventHandlers(); 340 | return this; 341 | } else { 342 | delete events[type]; 343 | } 344 | } else { 345 | spliceOne(list, position); 346 | } 347 | 348 | if (events.removeListener) 349 | this.emit('removeListener', type, originalListener || listener); 350 | } 351 | 352 | return this; 353 | }; 354 | 355 | EventEmitter.prototype.removeAllListeners = 356 | function removeAllListeners(type) { 357 | var listeners, events; 358 | 359 | events = this._events; 360 | if (!events) 361 | return this; 362 | 363 | // not listening for removeListener, no need to emit 364 | if (!events.removeListener) { 365 | if (arguments.length === 0) { 366 | this._events = new EventHandlers(); 367 | this._eventsCount = 0; 368 | } else if (events[type]) { 369 | if (--this._eventsCount === 0) 370 | this._events = new EventHandlers(); 371 | else 372 | delete events[type]; 373 | } 374 | return this; 375 | } 376 | 377 | // emit removeListener for all listeners on all events 378 | if (arguments.length === 0) { 379 | var keys = Object.keys(events); 380 | for (var i = 0, key; i < keys.length; ++i) { 381 | key = keys[i]; 382 | if (key === 'removeListener') continue; 383 | this.removeAllListeners(key); 384 | } 385 | this.removeAllListeners('removeListener'); 386 | this._events = new EventHandlers(); 387 | this._eventsCount = 0; 388 | return this; 389 | } 390 | 391 | listeners = events[type]; 392 | 393 | if (typeof listeners === 'function') { 394 | this.removeListener(type, listeners); 395 | } else if (listeners) { 396 | // LIFO order 397 | do { 398 | this.removeListener(type, listeners[listeners.length - 1]); 399 | } while (listeners[0]); 400 | } 401 | 402 | return this; 403 | }; 404 | 405 | EventEmitter.prototype.listeners = function listeners(type) { 406 | var evlistener; 407 | var ret; 408 | var events = this._events; 409 | 410 | if (!events) 411 | ret = []; 412 | else { 413 | evlistener = events[type]; 414 | if (!evlistener) 415 | ret = []; 416 | else if (typeof evlistener === 'function') 417 | ret = [evlistener.listener || evlistener]; 418 | else 419 | ret = unwrapListeners(evlistener); 420 | } 421 | 422 | return ret; 423 | }; 424 | 425 | EventEmitter.listenerCount = function(emitter, type) { 426 | if (typeof emitter.listenerCount === 'function') { 427 | return emitter.listenerCount(type); 428 | } else { 429 | return listenerCount.call(emitter, type); 430 | } 431 | }; 432 | 433 | EventEmitter.prototype.listenerCount = listenerCount; 434 | function listenerCount(type) { 435 | var events = this._events; 436 | 437 | if (events) { 438 | var evlistener = events[type]; 439 | 440 | if (typeof evlistener === 'function') { 441 | return 1; 442 | } else if (evlistener) { 443 | return evlistener.length; 444 | } 445 | } 446 | 447 | return 0; 448 | } 449 | 450 | EventEmitter.prototype.eventNames = function eventNames() { 451 | return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : []; 452 | }; 453 | 454 | // About 1.5x faster than the two-arg version of Array#splice(). 455 | function spliceOne(list, index) { 456 | for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) 457 | list[i] = list[k]; 458 | list.pop(); 459 | } 460 | 461 | function arrayClone(arr, i) { 462 | var copy = new Array(i); 463 | while (i--) 464 | copy[i] = arr[i]; 465 | return copy; 466 | } 467 | 468 | function unwrapListeners(arr) { 469 | var ret = new Array(arr.length); 470 | for (var i = 0; i < ret.length; ++i) { 471 | ret[i] = arr[i].listener || arr[i]; 472 | } 473 | return ret; 474 | } 475 | 476 | var classCallCheck = function (instance, Constructor) { 477 | if (!(instance instanceof Constructor)) { 478 | throw new TypeError("Cannot call a class as a function"); 479 | } 480 | }; 481 | 482 | var createClass = function () { 483 | function defineProperties(target, props) { 484 | for (var i = 0; i < props.length; i++) { 485 | var descriptor = props[i]; 486 | descriptor.enumerable = descriptor.enumerable || false; 487 | descriptor.configurable = true; 488 | if ("value" in descriptor) descriptor.writable = true; 489 | Object.defineProperty(target, descriptor.key, descriptor); 490 | } 491 | } 492 | 493 | return function (Constructor, protoProps, staticProps) { 494 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 495 | if (staticProps) defineProperties(Constructor, staticProps); 496 | return Constructor; 497 | }; 498 | }(); 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | var inherits = function (subClass, superClass) { 509 | if (typeof superClass !== "function" && superClass !== null) { 510 | throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); 511 | } 512 | 513 | subClass.prototype = Object.create(superClass && superClass.prototype, { 514 | constructor: { 515 | value: subClass, 516 | enumerable: false, 517 | writable: true, 518 | configurable: true 519 | } 520 | }); 521 | if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 522 | }; 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | var possibleConstructorReturn = function (self, call) { 535 | if (!self) { 536 | throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 537 | } 538 | 539 | return call && (typeof call === "object" || typeof call === "function") ? call : self; 540 | }; 541 | 542 | var RULE_TYPE = "rule"; 543 | var MEDIA_TYPE = "media"; 544 | 545 | var CssTreeWalker = function (_EventEmitter) { 546 | inherits(CssTreeWalker, _EventEmitter); 547 | 548 | function CssTreeWalker(code, plugins) { 549 | classCallCheck(this, CssTreeWalker); 550 | 551 | var _this = possibleConstructorReturn(this, (CssTreeWalker.__proto__ || Object.getPrototypeOf(CssTreeWalker)).call(this)); 552 | 553 | _this.startingSource = code; 554 | _this.ast = null; 555 | plugins.forEach(function (plugin) { 556 | plugin.initialize(_this); 557 | }); 558 | return _this; 559 | } 560 | 561 | createClass(CssTreeWalker, [{ 562 | key: "beginReading", 563 | value: function beginReading() { 564 | this.ast = rework(this.startingSource).use(this.readPlugin.bind(this)); 565 | } 566 | }, { 567 | key: "readPlugin", 568 | value: function readPlugin(tree) { 569 | this.readRules(tree.rules); 570 | this.removeEmptyRules(tree.rules); 571 | } 572 | }, { 573 | key: "readRules", 574 | value: function readRules(rules) { 575 | var _iteratorNormalCompletion = true; 576 | var _didIteratorError = false; 577 | var _iteratorError = undefined; 578 | 579 | try { 580 | for (var _iterator = rules[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 581 | var rule = _step.value; 582 | 583 | if (rule.type === RULE_TYPE) { 584 | this.emit("readRule", rule.selectors, rule); 585 | } 586 | if (rule.type === MEDIA_TYPE) { 587 | this.readRules(rule.rules); 588 | } 589 | } 590 | } catch (err) { 591 | _didIteratorError = true; 592 | _iteratorError = err; 593 | } finally { 594 | try { 595 | if (!_iteratorNormalCompletion && _iterator.return) { 596 | _iterator.return(); 597 | } 598 | } finally { 599 | if (_didIteratorError) { 600 | throw _iteratorError; 601 | } 602 | } 603 | } 604 | } 605 | }, { 606 | key: "removeEmptyRules", 607 | value: function removeEmptyRules(rules) { 608 | var emptyRules = []; 609 | 610 | var _iteratorNormalCompletion2 = true; 611 | var _didIteratorError2 = false; 612 | var _iteratorError2 = undefined; 613 | 614 | try { 615 | for (var _iterator2 = rules[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { 616 | var rule = _step2.value; 617 | 618 | var ruleType = rule.type; 619 | 620 | if (ruleType === RULE_TYPE && rule.selectors.length === 0) { 621 | emptyRules.push(rule); 622 | } 623 | if (ruleType === MEDIA_TYPE) { 624 | this.removeEmptyRules(rule.rules); 625 | if (rule.rules.length === 0) { 626 | emptyRules.push(rule); 627 | } 628 | } 629 | } 630 | } catch (err) { 631 | _didIteratorError2 = true; 632 | _iteratorError2 = err; 633 | } finally { 634 | try { 635 | if (!_iteratorNormalCompletion2 && _iterator2.return) { 636 | _iterator2.return(); 637 | } 638 | } finally { 639 | if (_didIteratorError2) { 640 | throw _iteratorError2; 641 | } 642 | } 643 | } 644 | 645 | emptyRules.forEach(function (emptyRule) { 646 | var index = rules.indexOf(emptyRule); 647 | rules.splice(index, 1); 648 | }); 649 | } 650 | }, { 651 | key: "toString", 652 | value: function toString() { 653 | if (this.ast) { 654 | return this.ast.toString().replace(/,\n/g, ","); 655 | } 656 | return ""; 657 | } 658 | }]); 659 | return CssTreeWalker; 660 | }(EventEmitter); 661 | 662 | var UglifyJS = require("uglify-js"); 663 | var fs$1 = require("fs"); 664 | var compressCode = function compressCode(code) { 665 | try { 666 | // Try to minimize the code as much as possible, removing noise. 667 | var ast = UglifyJS.parse(code); 668 | ast.figure_out_scope(); 669 | var compressor = UglifyJS.Compressor({ warnings: false }); 670 | ast = ast.transform(compressor); 671 | ast.figure_out_scope(); 672 | ast.compute_char_frequency(); 673 | ast.mangle_names({ toplevel: true }); 674 | code = ast.print_to_string().toLowerCase(); 675 | } catch (e) { 676 | // If compression fails, assume it's not a JS file and return the full code. 677 | } 678 | return code.toLowerCase(); 679 | }; 680 | 681 | var concatFiles = function concatFiles(files, options) { 682 | return files.reduce(function (total, file) { 683 | var code = ""; 684 | try { 685 | code = fs$1.readFileSync(file, "utf8"); 686 | code = options.compress ? compressCode(code) : code; 687 | } catch (e) { 688 | console.warn(e.message); 689 | } 690 | return "" + total + code + " "; 691 | }, ""); 692 | }; 693 | 694 | var getFilesFromPatternArray = function getFilesFromPatternArray(fileArray) { 695 | var sourceFiles = {}; 696 | var _iteratorNormalCompletion = true; 697 | var _didIteratorError = false; 698 | var _iteratorError = undefined; 699 | 700 | try { 701 | for (var _iterator = fileArray[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 702 | var string = _step.value; 703 | 704 | try { 705 | // See if string is a filepath, not a file pattern. 706 | fs$1.statSync(string); 707 | sourceFiles[string] = true; 708 | } catch (e) { 709 | var files = glob.sync(string); 710 | files.forEach(function (file) { 711 | sourceFiles[file] = true; 712 | }); 713 | } 714 | } 715 | } catch (err) { 716 | _didIteratorError = true; 717 | _iteratorError = err; 718 | } finally { 719 | try { 720 | if (!_iteratorNormalCompletion && _iterator.return) { 721 | _iterator.return(); 722 | } 723 | } finally { 724 | if (_didIteratorError) { 725 | throw _iteratorError; 726 | } 727 | } 728 | } 729 | 730 | return Object.keys(sourceFiles); 731 | }; 732 | 733 | var filesToSource = function filesToSource(files, type) { 734 | var isContent = type === "content"; 735 | var options = { compress: isContent }; 736 | if (Array.isArray(files)) { 737 | files = getFilesFromPatternArray(files); 738 | return concatFiles(files, options); 739 | } 740 | // 'files' is already a source string. 741 | return isContent ? compressCode(files) : files; 742 | }; 743 | 744 | var FileUtil = { 745 | concatFiles: concatFiles, 746 | filesToSource: filesToSource, 747 | getFilesFromPatternArray: getFilesFromPatternArray 748 | }; 749 | 750 | var startTime = void 0; 751 | var beginningLength = void 0; 752 | 753 | var printInfo = function printInfo(endingLength) { 754 | var sizeReduction = ((beginningLength - endingLength) / beginningLength * 100).toFixed(1); 755 | console.log("\n ________________________________________________\n |\n | PurifyCSS has reduced the file size by ~ " + sizeReduction + "% \n |\n ________________________________________________\n "); 756 | }; 757 | 758 | var printRejected = function printRejected(rejectedTwigs) { 759 | console.log("\n ________________________________________________\n |\n | PurifyCSS - Rejected selectors: \n | " + rejectedTwigs.join("\n |\t") + "\n |\n ________________________________________________\n "); 760 | }; 761 | 762 | var startLog = function startLog(cssLength) { 763 | startTime = new Date(); 764 | beginningLength = cssLength; 765 | }; 766 | 767 | var PrintUtil = { 768 | printInfo: printInfo, 769 | printRejected: printRejected, 770 | startLog: startLog 771 | }; 772 | 773 | var addWord = function addWord(words, word) { 774 | if (word) words.push(word); 775 | }; 776 | 777 | var getAllWordsInContent = function getAllWordsInContent(content) { 778 | var used = { 779 | // Always include html and body. 780 | html: true, 781 | body: true 782 | }; 783 | var words = content.split(/[^a-zA-Z0-9\\@+]/g); 784 | var _iteratorNormalCompletion = true; 785 | var _didIteratorError = false; 786 | var _iteratorError = undefined; 787 | 788 | try { 789 | for (var _iterator = words[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 790 | var word = _step.value; 791 | 792 | if (word && word !== "+") { 793 | used[word] = true; 794 | } 795 | } 796 | } catch (err) { 797 | _didIteratorError = true; 798 | _iteratorError = err; 799 | } finally { 800 | try { 801 | if (!_iteratorNormalCompletion && _iterator.return) { 802 | _iterator.return(); 803 | } 804 | } finally { 805 | if (_didIteratorError) { 806 | throw _iteratorError; 807 | } 808 | } 809 | } 810 | 811 | return used; 812 | }; 813 | 814 | var getAllWordsInSelector = function getAllWordsInSelector(selector) { 815 | // Remove attr selectors. "a[href...]"" will become "a". 816 | selector = selector.replace(/\[(.+?)\]/g, "").toLowerCase(); 817 | // If complex attr selector (has a bracket in it) just leave 818 | // the selector in. ¯\_(ツ)_/¯ 819 | if (selector.includes("[") || selector.includes("]")) { 820 | return []; 821 | } 822 | var skipNextWord = false, 823 | word = "", 824 | words = []; 825 | 826 | var last_letter = ''; 827 | var _iteratorNormalCompletion2 = true; 828 | var _didIteratorError2 = false; 829 | var _iteratorError2 = undefined; 830 | 831 | try { 832 | for (var _iterator2 = selector[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { 833 | var letter = _step2.value; 834 | 835 | if (skipNextWord && !/[ #.]/.test(letter)) continue; 836 | // If pseudoclass or universal selector, skip the next word 837 | if (/[:*]/.test(letter) && last_letter != '\\') { 838 | addWord(words, word.replace('\\', '')); 839 | word = ""; 840 | skipNextWord = true; 841 | continue; 842 | } 843 | if (/[a-zA-Z0-9\\@+]/.test(letter)) { 844 | word += letter; 845 | last_letter = letter; 846 | } else { 847 | addWord(words, word.replace('\\', '')); 848 | word = ""; 849 | skipNextWord = false; 850 | } 851 | } 852 | } catch (err) { 853 | _didIteratorError2 = true; 854 | _iteratorError2 = err; 855 | } finally { 856 | try { 857 | if (!_iteratorNormalCompletion2 && _iterator2.return) { 858 | _iterator2.return(); 859 | } 860 | } finally { 861 | if (_didIteratorError2) { 862 | throw _iteratorError2; 863 | } 864 | } 865 | } 866 | 867 | addWord(words, word); 868 | return words; 869 | }; 870 | 871 | var isWildcardWhitelistSelector = function isWildcardWhitelistSelector(selector) { 872 | return selector[0] === "*" && selector[selector.length - 1] === "*"; 873 | }; 874 | 875 | var hasWhitelistMatch = function hasWhitelistMatch(selector, whitelist) { 876 | var _iteratorNormalCompletion = true; 877 | var _didIteratorError = false; 878 | var _iteratorError = undefined; 879 | 880 | try { 881 | for (var _iterator = whitelist[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 882 | var el = _step.value; 883 | 884 | if (selector.includes(el)) return true; 885 | } 886 | } catch (err) { 887 | _didIteratorError = true; 888 | _iteratorError = err; 889 | } finally { 890 | try { 891 | if (!_iteratorNormalCompletion && _iterator.return) { 892 | _iterator.return(); 893 | } 894 | } finally { 895 | if (_didIteratorError) { 896 | throw _iteratorError; 897 | } 898 | } 899 | } 900 | 901 | return false; 902 | }; 903 | 904 | var SelectorFilter = function () { 905 | function SelectorFilter(contentWords, whitelist) { 906 | classCallCheck(this, SelectorFilter); 907 | 908 | this.contentWords = contentWords; 909 | this.rejectedSelectors = []; 910 | this.wildcardWhitelist = []; 911 | this.parseWhitelist(whitelist); 912 | } 913 | 914 | createClass(SelectorFilter, [{ 915 | key: "initialize", 916 | value: function initialize(CssSyntaxTree) { 917 | CssSyntaxTree.on("readRule", this.parseRule.bind(this)); 918 | } 919 | }, { 920 | key: "parseWhitelist", 921 | value: function parseWhitelist(whitelist) { 922 | var _this = this; 923 | 924 | whitelist.forEach(function (whitelistSelector) { 925 | whitelistSelector = whitelistSelector.toLowerCase(); 926 | 927 | if (isWildcardWhitelistSelector(whitelistSelector)) { 928 | // If '*button*' then push 'button' onto list. 929 | _this.wildcardWhitelist.push(whitelistSelector.substr(1, whitelistSelector.length - 2)); 930 | } else { 931 | getAllWordsInSelector(whitelistSelector).forEach(function (word) { 932 | _this.contentWords[word] = true; 933 | }); 934 | } 935 | }); 936 | } 937 | }, { 938 | key: "parseRule", 939 | value: function parseRule(selectors, rule) { 940 | rule.selectors = this.filterSelectors(selectors); 941 | } 942 | }, { 943 | key: "filterSelectors", 944 | value: function filterSelectors(selectors) { 945 | var contentWords = this.contentWords, 946 | rejectedSelectors = this.rejectedSelectors, 947 | wildcardWhitelist = this.wildcardWhitelist, 948 | usedSelectors = []; 949 | 950 | // console.log(selectors) 951 | selectors.forEach(function (selector) { 952 | if (hasWhitelistMatch(selector, wildcardWhitelist)) { 953 | usedSelectors.push(selector); 954 | return; 955 | } 956 | var words = getAllWordsInSelector(selector), 957 | usedWords = words.filter(function (word) { 958 | return contentWords[word.replace('\\', '')]; 959 | }); 960 | 961 | // console.log(words, usedWords, selector) 962 | if (usedWords.length === words.length) { 963 | usedSelectors.push(selector); 964 | } else { 965 | rejectedSelectors.push(selector); 966 | } 967 | }); 968 | 969 | return usedSelectors; 970 | } 971 | }]); 972 | return SelectorFilter; 973 | }(); 974 | 975 | var fs = require("fs"); 976 | var OPTIONS = { 977 | output: false, 978 | minify: false, 979 | info: false, 980 | rejected: false, 981 | whitelist: [], 982 | cleanCssOptions: {} 983 | }; 984 | 985 | var getOptions = function getOptions() { 986 | var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 987 | 988 | var opt = {}; 989 | for (var option in OPTIONS) { 990 | opt[option] = options[option] || OPTIONS[option]; 991 | } 992 | return opt; 993 | }; 994 | 995 | var minify = function minify(cssSource, options) { 996 | return new CleanCss(options).minify(cssSource).styles; 997 | }; 998 | 999 | var purify = function purify(searchThrough, css, options, callback) { 1000 | if (typeof options === "function") { 1001 | callback = options; 1002 | options = {}; 1003 | } 1004 | options = getOptions(options); 1005 | var cssString = FileUtil.filesToSource(css, "css"), 1006 | content = FileUtil.filesToSource(searchThrough, "content"); 1007 | PrintUtil.startLog(minify(cssString).length); 1008 | var wordsInContent = getAllWordsInContent(content), 1009 | selectorFilter = new SelectorFilter(wordsInContent, options.whitelist), 1010 | tree = new CssTreeWalker(cssString, [selectorFilter]); 1011 | tree.beginReading(); 1012 | var source = tree.toString(); 1013 | 1014 | source = options.minify ? minify(source, options.cleanCssOptions) : source; 1015 | 1016 | // Option info = true 1017 | if (options.info) { 1018 | if (options.minify) { 1019 | PrintUtil.printInfo(source.length); 1020 | } else { 1021 | PrintUtil.printInfo(minify(source, options.cleanCssOptions).length); 1022 | } 1023 | } 1024 | 1025 | // Option rejected = true 1026 | if (options.rejected && selectorFilter.rejectedSelectors.length) { 1027 | PrintUtil.printRejected(selectorFilter.rejectedSelectors); 1028 | } 1029 | 1030 | if (options.output) { 1031 | fs.writeFile(options.output, source, function (err) { 1032 | if (err) return err; 1033 | }); 1034 | } else { 1035 | return callback ? callback(source) : source; 1036 | } 1037 | }; 1038 | 1039 | export default purify; 1040 | -------------------------------------------------------------------------------- /lib/purifycss.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } 4 | 5 | var CleanCss = _interopDefault(require('clean-css')); 6 | var rework = _interopDefault(require('rework')); 7 | var glob = _interopDefault(require('glob')); 8 | 9 | var domain; 10 | 11 | // This constructor is used to store event handlers. Instantiating this is 12 | // faster than explicitly calling `Object.create(null)` to get a "clean" empty 13 | // object (tested with v8 v4.9). 14 | function EventHandlers() {} 15 | EventHandlers.prototype = Object.create(null); 16 | 17 | function EventEmitter() { 18 | EventEmitter.init.call(this); 19 | } 20 | // nodejs oddity 21 | // require('events') === require('events').EventEmitter 22 | EventEmitter.EventEmitter = EventEmitter; 23 | 24 | EventEmitter.usingDomains = false; 25 | 26 | EventEmitter.prototype.domain = undefined; 27 | EventEmitter.prototype._events = undefined; 28 | EventEmitter.prototype._maxListeners = undefined; 29 | 30 | // By default EventEmitters will print a warning if more than 10 listeners are 31 | // added to it. This is a useful default which helps finding memory leaks. 32 | EventEmitter.defaultMaxListeners = 10; 33 | 34 | EventEmitter.init = function() { 35 | this.domain = null; 36 | if (EventEmitter.usingDomains) { 37 | // if there is an active domain, then attach to it. 38 | if (domain.active && !(this instanceof domain.Domain)) { 39 | this.domain = domain.active; 40 | } 41 | } 42 | 43 | if (!this._events || this._events === Object.getPrototypeOf(this)._events) { 44 | this._events = new EventHandlers(); 45 | this._eventsCount = 0; 46 | } 47 | 48 | this._maxListeners = this._maxListeners || undefined; 49 | }; 50 | 51 | // Obviously not all Emitters should be limited to 10. This function allows 52 | // that to be increased. Set to zero for unlimited. 53 | EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { 54 | if (typeof n !== 'number' || n < 0 || isNaN(n)) 55 | throw new TypeError('"n" argument must be a positive number'); 56 | this._maxListeners = n; 57 | return this; 58 | }; 59 | 60 | function $getMaxListeners(that) { 61 | if (that._maxListeners === undefined) 62 | return EventEmitter.defaultMaxListeners; 63 | return that._maxListeners; 64 | } 65 | 66 | EventEmitter.prototype.getMaxListeners = function getMaxListeners() { 67 | return $getMaxListeners(this); 68 | }; 69 | 70 | // These standalone emit* functions are used to optimize calling of event 71 | // handlers for fast cases because emit() itself often has a variable number of 72 | // arguments and can be deoptimized because of that. These functions always have 73 | // the same number of arguments and thus do not get deoptimized, so the code 74 | // inside them can execute faster. 75 | function emitNone(handler, isFn, self) { 76 | if (isFn) 77 | handler.call(self); 78 | else { 79 | var len = handler.length; 80 | var listeners = arrayClone(handler, len); 81 | for (var i = 0; i < len; ++i) 82 | listeners[i].call(self); 83 | } 84 | } 85 | function emitOne(handler, isFn, self, arg1) { 86 | if (isFn) 87 | handler.call(self, arg1); 88 | else { 89 | var len = handler.length; 90 | var listeners = arrayClone(handler, len); 91 | for (var i = 0; i < len; ++i) 92 | listeners[i].call(self, arg1); 93 | } 94 | } 95 | function emitTwo(handler, isFn, self, arg1, arg2) { 96 | if (isFn) 97 | handler.call(self, arg1, arg2); 98 | else { 99 | var len = handler.length; 100 | var listeners = arrayClone(handler, len); 101 | for (var i = 0; i < len; ++i) 102 | listeners[i].call(self, arg1, arg2); 103 | } 104 | } 105 | function emitThree(handler, isFn, self, arg1, arg2, arg3) { 106 | if (isFn) 107 | handler.call(self, arg1, arg2, arg3); 108 | else { 109 | var len = handler.length; 110 | var listeners = arrayClone(handler, len); 111 | for (var i = 0; i < len; ++i) 112 | listeners[i].call(self, arg1, arg2, arg3); 113 | } 114 | } 115 | 116 | function emitMany(handler, isFn, self, args) { 117 | if (isFn) 118 | handler.apply(self, args); 119 | else { 120 | var len = handler.length; 121 | var listeners = arrayClone(handler, len); 122 | for (var i = 0; i < len; ++i) 123 | listeners[i].apply(self, args); 124 | } 125 | } 126 | 127 | EventEmitter.prototype.emit = function emit(type) { 128 | var er, handler, len, args, i, events, domain; 129 | var needDomainExit = false; 130 | var doError = (type === 'error'); 131 | 132 | events = this._events; 133 | if (events) 134 | doError = (doError && events.error == null); 135 | else if (!doError) 136 | return false; 137 | 138 | domain = this.domain; 139 | 140 | // If there is no 'error' event listener then throw. 141 | if (doError) { 142 | er = arguments[1]; 143 | if (domain) { 144 | if (!er) 145 | er = new Error('Uncaught, unspecified "error" event'); 146 | er.domainEmitter = this; 147 | er.domain = domain; 148 | er.domainThrown = false; 149 | domain.emit('error', er); 150 | } else if (er instanceof Error) { 151 | throw er; // Unhandled 'error' event 152 | } else { 153 | // At least give some kind of context to the user 154 | var err = new Error('Uncaught, unspecified "error" event. (' + er + ')'); 155 | err.context = er; 156 | throw err; 157 | } 158 | return false; 159 | } 160 | 161 | handler = events[type]; 162 | 163 | if (!handler) 164 | return false; 165 | 166 | var isFn = typeof handler === 'function'; 167 | len = arguments.length; 168 | switch (len) { 169 | // fast cases 170 | case 1: 171 | emitNone(handler, isFn, this); 172 | break; 173 | case 2: 174 | emitOne(handler, isFn, this, arguments[1]); 175 | break; 176 | case 3: 177 | emitTwo(handler, isFn, this, arguments[1], arguments[2]); 178 | break; 179 | case 4: 180 | emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]); 181 | break; 182 | // slower 183 | default: 184 | args = new Array(len - 1); 185 | for (i = 1; i < len; i++) 186 | args[i - 1] = arguments[i]; 187 | emitMany(handler, isFn, this, args); 188 | } 189 | 190 | if (needDomainExit) 191 | domain.exit(); 192 | 193 | return true; 194 | }; 195 | 196 | function _addListener(target, type, listener, prepend) { 197 | var m; 198 | var events; 199 | var existing; 200 | 201 | if (typeof listener !== 'function') 202 | throw new TypeError('"listener" argument must be a function'); 203 | 204 | events = target._events; 205 | if (!events) { 206 | events = target._events = new EventHandlers(); 207 | target._eventsCount = 0; 208 | } else { 209 | // To avoid recursion in the case that type === "newListener"! Before 210 | // adding it to the listeners, first emit "newListener". 211 | if (events.newListener) { 212 | target.emit('newListener', type, 213 | listener.listener ? listener.listener : listener); 214 | 215 | // Re-assign `events` because a newListener handler could have caused the 216 | // this._events to be assigned to a new object 217 | events = target._events; 218 | } 219 | existing = events[type]; 220 | } 221 | 222 | if (!existing) { 223 | // Optimize the case of one listener. Don't need the extra array object. 224 | existing = events[type] = listener; 225 | ++target._eventsCount; 226 | } else { 227 | if (typeof existing === 'function') { 228 | // Adding the second element, need to change to array. 229 | existing = events[type] = prepend ? [listener, existing] : 230 | [existing, listener]; 231 | } else { 232 | // If we've already got an array, just append. 233 | if (prepend) { 234 | existing.unshift(listener); 235 | } else { 236 | existing.push(listener); 237 | } 238 | } 239 | 240 | // Check for listener leak 241 | if (!existing.warned) { 242 | m = $getMaxListeners(target); 243 | if (m && m > 0 && existing.length > m) { 244 | existing.warned = true; 245 | var w = new Error('Possible EventEmitter memory leak detected. ' + 246 | existing.length + ' ' + type + ' listeners added. ' + 247 | 'Use emitter.setMaxListeners() to increase limit'); 248 | w.name = 'MaxListenersExceededWarning'; 249 | w.emitter = target; 250 | w.type = type; 251 | w.count = existing.length; 252 | emitWarning(w); 253 | } 254 | } 255 | } 256 | 257 | return target; 258 | } 259 | function emitWarning(e) { 260 | typeof console.warn === 'function' ? console.warn(e) : console.log(e); 261 | } 262 | EventEmitter.prototype.addListener = function addListener(type, listener) { 263 | return _addListener(this, type, listener, false); 264 | }; 265 | 266 | EventEmitter.prototype.on = EventEmitter.prototype.addListener; 267 | 268 | EventEmitter.prototype.prependListener = 269 | function prependListener(type, listener) { 270 | return _addListener(this, type, listener, true); 271 | }; 272 | 273 | function _onceWrap(target, type, listener) { 274 | var fired = false; 275 | function g() { 276 | target.removeListener(type, g); 277 | if (!fired) { 278 | fired = true; 279 | listener.apply(target, arguments); 280 | } 281 | } 282 | g.listener = listener; 283 | return g; 284 | } 285 | 286 | EventEmitter.prototype.once = function once(type, listener) { 287 | if (typeof listener !== 'function') 288 | throw new TypeError('"listener" argument must be a function'); 289 | this.on(type, _onceWrap(this, type, listener)); 290 | return this; 291 | }; 292 | 293 | EventEmitter.prototype.prependOnceListener = 294 | function prependOnceListener(type, listener) { 295 | if (typeof listener !== 'function') 296 | throw new TypeError('"listener" argument must be a function'); 297 | this.prependListener(type, _onceWrap(this, type, listener)); 298 | return this; 299 | }; 300 | 301 | // emits a 'removeListener' event iff the listener was removed 302 | EventEmitter.prototype.removeListener = 303 | function removeListener(type, listener) { 304 | var list, events, position, i, originalListener; 305 | 306 | if (typeof listener !== 'function') 307 | throw new TypeError('"listener" argument must be a function'); 308 | 309 | events = this._events; 310 | if (!events) 311 | return this; 312 | 313 | list = events[type]; 314 | if (!list) 315 | return this; 316 | 317 | if (list === listener || (list.listener && list.listener === listener)) { 318 | if (--this._eventsCount === 0) 319 | this._events = new EventHandlers(); 320 | else { 321 | delete events[type]; 322 | if (events.removeListener) 323 | this.emit('removeListener', type, list.listener || listener); 324 | } 325 | } else if (typeof list !== 'function') { 326 | position = -1; 327 | 328 | for (i = list.length; i-- > 0;) { 329 | if (list[i] === listener || 330 | (list[i].listener && list[i].listener === listener)) { 331 | originalListener = list[i].listener; 332 | position = i; 333 | break; 334 | } 335 | } 336 | 337 | if (position < 0) 338 | return this; 339 | 340 | if (list.length === 1) { 341 | list[0] = undefined; 342 | if (--this._eventsCount === 0) { 343 | this._events = new EventHandlers(); 344 | return this; 345 | } else { 346 | delete events[type]; 347 | } 348 | } else { 349 | spliceOne(list, position); 350 | } 351 | 352 | if (events.removeListener) 353 | this.emit('removeListener', type, originalListener || listener); 354 | } 355 | 356 | return this; 357 | }; 358 | 359 | EventEmitter.prototype.removeAllListeners = 360 | function removeAllListeners(type) { 361 | var listeners, events; 362 | 363 | events = this._events; 364 | if (!events) 365 | return this; 366 | 367 | // not listening for removeListener, no need to emit 368 | if (!events.removeListener) { 369 | if (arguments.length === 0) { 370 | this._events = new EventHandlers(); 371 | this._eventsCount = 0; 372 | } else if (events[type]) { 373 | if (--this._eventsCount === 0) 374 | this._events = new EventHandlers(); 375 | else 376 | delete events[type]; 377 | } 378 | return this; 379 | } 380 | 381 | // emit removeListener for all listeners on all events 382 | if (arguments.length === 0) { 383 | var keys = Object.keys(events); 384 | for (var i = 0, key; i < keys.length; ++i) { 385 | key = keys[i]; 386 | if (key === 'removeListener') continue; 387 | this.removeAllListeners(key); 388 | } 389 | this.removeAllListeners('removeListener'); 390 | this._events = new EventHandlers(); 391 | this._eventsCount = 0; 392 | return this; 393 | } 394 | 395 | listeners = events[type]; 396 | 397 | if (typeof listeners === 'function') { 398 | this.removeListener(type, listeners); 399 | } else if (listeners) { 400 | // LIFO order 401 | do { 402 | this.removeListener(type, listeners[listeners.length - 1]); 403 | } while (listeners[0]); 404 | } 405 | 406 | return this; 407 | }; 408 | 409 | EventEmitter.prototype.listeners = function listeners(type) { 410 | var evlistener; 411 | var ret; 412 | var events = this._events; 413 | 414 | if (!events) 415 | ret = []; 416 | else { 417 | evlistener = events[type]; 418 | if (!evlistener) 419 | ret = []; 420 | else if (typeof evlistener === 'function') 421 | ret = [evlistener.listener || evlistener]; 422 | else 423 | ret = unwrapListeners(evlistener); 424 | } 425 | 426 | return ret; 427 | }; 428 | 429 | EventEmitter.listenerCount = function(emitter, type) { 430 | if (typeof emitter.listenerCount === 'function') { 431 | return emitter.listenerCount(type); 432 | } else { 433 | return listenerCount.call(emitter, type); 434 | } 435 | }; 436 | 437 | EventEmitter.prototype.listenerCount = listenerCount; 438 | function listenerCount(type) { 439 | var events = this._events; 440 | 441 | if (events) { 442 | var evlistener = events[type]; 443 | 444 | if (typeof evlistener === 'function') { 445 | return 1; 446 | } else if (evlistener) { 447 | return evlistener.length; 448 | } 449 | } 450 | 451 | return 0; 452 | } 453 | 454 | EventEmitter.prototype.eventNames = function eventNames() { 455 | return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : []; 456 | }; 457 | 458 | // About 1.5x faster than the two-arg version of Array#splice(). 459 | function spliceOne(list, index) { 460 | for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) 461 | list[i] = list[k]; 462 | list.pop(); 463 | } 464 | 465 | function arrayClone(arr, i) { 466 | var copy = new Array(i); 467 | while (i--) 468 | copy[i] = arr[i]; 469 | return copy; 470 | } 471 | 472 | function unwrapListeners(arr) { 473 | var ret = new Array(arr.length); 474 | for (var i = 0; i < ret.length; ++i) { 475 | ret[i] = arr[i].listener || arr[i]; 476 | } 477 | return ret; 478 | } 479 | 480 | var classCallCheck = function (instance, Constructor) { 481 | if (!(instance instanceof Constructor)) { 482 | throw new TypeError("Cannot call a class as a function"); 483 | } 484 | }; 485 | 486 | var createClass = function () { 487 | function defineProperties(target, props) { 488 | for (var i = 0; i < props.length; i++) { 489 | var descriptor = props[i]; 490 | descriptor.enumerable = descriptor.enumerable || false; 491 | descriptor.configurable = true; 492 | if ("value" in descriptor) descriptor.writable = true; 493 | Object.defineProperty(target, descriptor.key, descriptor); 494 | } 495 | } 496 | 497 | return function (Constructor, protoProps, staticProps) { 498 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 499 | if (staticProps) defineProperties(Constructor, staticProps); 500 | return Constructor; 501 | }; 502 | }(); 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | var inherits = function (subClass, superClass) { 513 | if (typeof superClass !== "function" && superClass !== null) { 514 | throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); 515 | } 516 | 517 | subClass.prototype = Object.create(superClass && superClass.prototype, { 518 | constructor: { 519 | value: subClass, 520 | enumerable: false, 521 | writable: true, 522 | configurable: true 523 | } 524 | }); 525 | if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 526 | }; 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | var possibleConstructorReturn = function (self, call) { 539 | if (!self) { 540 | throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 541 | } 542 | 543 | return call && (typeof call === "object" || typeof call === "function") ? call : self; 544 | }; 545 | 546 | var RULE_TYPE = "rule"; 547 | var MEDIA_TYPE = "media"; 548 | 549 | var CssTreeWalker = function (_EventEmitter) { 550 | inherits(CssTreeWalker, _EventEmitter); 551 | 552 | function CssTreeWalker(code, plugins) { 553 | classCallCheck(this, CssTreeWalker); 554 | 555 | var _this = possibleConstructorReturn(this, (CssTreeWalker.__proto__ || Object.getPrototypeOf(CssTreeWalker)).call(this)); 556 | 557 | _this.startingSource = code; 558 | _this.ast = null; 559 | plugins.forEach(function (plugin) { 560 | plugin.initialize(_this); 561 | }); 562 | return _this; 563 | } 564 | 565 | createClass(CssTreeWalker, [{ 566 | key: "beginReading", 567 | value: function beginReading() { 568 | this.ast = rework(this.startingSource).use(this.readPlugin.bind(this)); 569 | } 570 | }, { 571 | key: "readPlugin", 572 | value: function readPlugin(tree) { 573 | this.readRules(tree.rules); 574 | this.removeEmptyRules(tree.rules); 575 | } 576 | }, { 577 | key: "readRules", 578 | value: function readRules(rules) { 579 | var _iteratorNormalCompletion = true; 580 | var _didIteratorError = false; 581 | var _iteratorError = undefined; 582 | 583 | try { 584 | for (var _iterator = rules[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 585 | var rule = _step.value; 586 | 587 | if (rule.type === RULE_TYPE) { 588 | this.emit("readRule", rule.selectors, rule); 589 | } 590 | if (rule.type === MEDIA_TYPE) { 591 | this.readRules(rule.rules); 592 | } 593 | } 594 | } catch (err) { 595 | _didIteratorError = true; 596 | _iteratorError = err; 597 | } finally { 598 | try { 599 | if (!_iteratorNormalCompletion && _iterator.return) { 600 | _iterator.return(); 601 | } 602 | } finally { 603 | if (_didIteratorError) { 604 | throw _iteratorError; 605 | } 606 | } 607 | } 608 | } 609 | }, { 610 | key: "removeEmptyRules", 611 | value: function removeEmptyRules(rules) { 612 | var emptyRules = []; 613 | 614 | var _iteratorNormalCompletion2 = true; 615 | var _didIteratorError2 = false; 616 | var _iteratorError2 = undefined; 617 | 618 | try { 619 | for (var _iterator2 = rules[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { 620 | var rule = _step2.value; 621 | 622 | var ruleType = rule.type; 623 | 624 | if (ruleType === RULE_TYPE && rule.selectors.length === 0) { 625 | emptyRules.push(rule); 626 | } 627 | if (ruleType === MEDIA_TYPE) { 628 | this.removeEmptyRules(rule.rules); 629 | if (rule.rules.length === 0) { 630 | emptyRules.push(rule); 631 | } 632 | } 633 | } 634 | } catch (err) { 635 | _didIteratorError2 = true; 636 | _iteratorError2 = err; 637 | } finally { 638 | try { 639 | if (!_iteratorNormalCompletion2 && _iterator2.return) { 640 | _iterator2.return(); 641 | } 642 | } finally { 643 | if (_didIteratorError2) { 644 | throw _iteratorError2; 645 | } 646 | } 647 | } 648 | 649 | emptyRules.forEach(function (emptyRule) { 650 | var index = rules.indexOf(emptyRule); 651 | rules.splice(index, 1); 652 | }); 653 | } 654 | }, { 655 | key: "toString", 656 | value: function toString() { 657 | if (this.ast) { 658 | return this.ast.toString().replace(/,\n/g, ","); 659 | } 660 | return ""; 661 | } 662 | }]); 663 | return CssTreeWalker; 664 | }(EventEmitter); 665 | 666 | var UglifyJS = require("uglify-js"); 667 | var fs$1 = require("fs"); 668 | var compressCode = function compressCode(code) { 669 | try { 670 | // Try to minimize the code as much as possible, removing noise. 671 | var ast = UglifyJS.parse(code); 672 | ast.figure_out_scope(); 673 | var compressor = UglifyJS.Compressor({ warnings: false }); 674 | ast = ast.transform(compressor); 675 | ast.figure_out_scope(); 676 | ast.compute_char_frequency(); 677 | ast.mangle_names({ toplevel: true }); 678 | code = ast.print_to_string().toLowerCase(); 679 | } catch (e) { 680 | // If compression fails, assume it's not a JS file and return the full code. 681 | } 682 | return code.toLowerCase(); 683 | }; 684 | 685 | var concatFiles = function concatFiles(files, options) { 686 | return files.reduce(function (total, file) { 687 | var code = ""; 688 | try { 689 | code = fs$1.readFileSync(file, "utf8"); 690 | code = options.compress ? compressCode(code) : code; 691 | } catch (e) { 692 | console.warn(e.message); 693 | } 694 | return "" + total + code + " "; 695 | }, ""); 696 | }; 697 | 698 | var getFilesFromPatternArray = function getFilesFromPatternArray(fileArray) { 699 | var sourceFiles = {}; 700 | var _iteratorNormalCompletion = true; 701 | var _didIteratorError = false; 702 | var _iteratorError = undefined; 703 | 704 | try { 705 | for (var _iterator = fileArray[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 706 | var string = _step.value; 707 | 708 | try { 709 | // See if string is a filepath, not a file pattern. 710 | fs$1.statSync(string); 711 | sourceFiles[string] = true; 712 | } catch (e) { 713 | var files = glob.sync(string); 714 | files.forEach(function (file) { 715 | sourceFiles[file] = true; 716 | }); 717 | } 718 | } 719 | } catch (err) { 720 | _didIteratorError = true; 721 | _iteratorError = err; 722 | } finally { 723 | try { 724 | if (!_iteratorNormalCompletion && _iterator.return) { 725 | _iterator.return(); 726 | } 727 | } finally { 728 | if (_didIteratorError) { 729 | throw _iteratorError; 730 | } 731 | } 732 | } 733 | 734 | return Object.keys(sourceFiles); 735 | }; 736 | 737 | var filesToSource = function filesToSource(files, type) { 738 | var isContent = type === "content"; 739 | var options = { compress: isContent }; 740 | if (Array.isArray(files)) { 741 | files = getFilesFromPatternArray(files); 742 | return concatFiles(files, options); 743 | } 744 | // 'files' is already a source string. 745 | return isContent ? compressCode(files) : files; 746 | }; 747 | 748 | var FileUtil = { 749 | concatFiles: concatFiles, 750 | filesToSource: filesToSource, 751 | getFilesFromPatternArray: getFilesFromPatternArray 752 | }; 753 | 754 | var startTime = void 0; 755 | var beginningLength = void 0; 756 | 757 | var printInfo = function printInfo(endingLength) { 758 | var sizeReduction = ((beginningLength - endingLength) / beginningLength * 100).toFixed(1); 759 | console.log("\n ________________________________________________\n |\n | PurifyCSS has reduced the file size by ~ " + sizeReduction + "% \n |\n ________________________________________________\n "); 760 | }; 761 | 762 | var printRejected = function printRejected(rejectedTwigs) { 763 | console.log("\n ________________________________________________\n |\n | PurifyCSS - Rejected selectors: \n | " + rejectedTwigs.join("\n |\t") + "\n |\n ________________________________________________\n "); 764 | }; 765 | 766 | var startLog = function startLog(cssLength) { 767 | startTime = new Date(); 768 | beginningLength = cssLength; 769 | }; 770 | 771 | var PrintUtil = { 772 | printInfo: printInfo, 773 | printRejected: printRejected, 774 | startLog: startLog 775 | }; 776 | 777 | var addWord = function addWord(words, word) { 778 | if (word) words.push(word); 779 | }; 780 | 781 | var getAllWordsInContent = function getAllWordsInContent(content) { 782 | var used = { 783 | // Always include html and body. 784 | html: true, 785 | body: true 786 | }; 787 | var words = content.split(/[^a-zA-Z0-9\\@+]/g); 788 | var _iteratorNormalCompletion = true; 789 | var _didIteratorError = false; 790 | var _iteratorError = undefined; 791 | 792 | try { 793 | for (var _iterator = words[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 794 | var word = _step.value; 795 | 796 | if (word && word !== "+") { 797 | used[word] = true; 798 | } 799 | } 800 | } catch (err) { 801 | _didIteratorError = true; 802 | _iteratorError = err; 803 | } finally { 804 | try { 805 | if (!_iteratorNormalCompletion && _iterator.return) { 806 | _iterator.return(); 807 | } 808 | } finally { 809 | if (_didIteratorError) { 810 | throw _iteratorError; 811 | } 812 | } 813 | } 814 | 815 | return used; 816 | }; 817 | 818 | var getAllWordsInSelector = function getAllWordsInSelector(selector) { 819 | // Remove attr selectors. "a[href...]"" will become "a". 820 | selector = selector.replace(/\[(.+?)\]/g, "").toLowerCase(); 821 | // If complex attr selector (has a bracket in it) just leave 822 | // the selector in. ¯\_(ツ)_/¯ 823 | if (selector.includes("[") || selector.includes("]")) { 824 | return []; 825 | } 826 | var skipNextWord = false, 827 | word = "", 828 | words = []; 829 | 830 | var last_letter = ''; 831 | var _iteratorNormalCompletion2 = true; 832 | var _didIteratorError2 = false; 833 | var _iteratorError2 = undefined; 834 | 835 | try { 836 | for (var _iterator2 = selector[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { 837 | var letter = _step2.value; 838 | 839 | if (skipNextWord && !/[ #.]/.test(letter)) continue; 840 | // If pseudoclass or universal selector, skip the next word 841 | if (/[:*]/.test(letter) && last_letter != '\\') { 842 | addWord(words, word.replace('\\', '')); 843 | word = ""; 844 | skipNextWord = true; 845 | continue; 846 | } 847 | if (/[a-zA-Z0-9\\@+]/.test(letter)) { 848 | word += letter; 849 | last_letter = letter; 850 | } else { 851 | addWord(words, word.replace('\\', '')); 852 | word = ""; 853 | skipNextWord = false; 854 | } 855 | } 856 | } catch (err) { 857 | _didIteratorError2 = true; 858 | _iteratorError2 = err; 859 | } finally { 860 | try { 861 | if (!_iteratorNormalCompletion2 && _iterator2.return) { 862 | _iterator2.return(); 863 | } 864 | } finally { 865 | if (_didIteratorError2) { 866 | throw _iteratorError2; 867 | } 868 | } 869 | } 870 | 871 | addWord(words, word); 872 | return words; 873 | }; 874 | 875 | var isWildcardWhitelistSelector = function isWildcardWhitelistSelector(selector) { 876 | return selector[0] === "*" && selector[selector.length - 1] === "*"; 877 | }; 878 | 879 | var hasWhitelistMatch = function hasWhitelistMatch(selector, whitelist) { 880 | var _iteratorNormalCompletion = true; 881 | var _didIteratorError = false; 882 | var _iteratorError = undefined; 883 | 884 | try { 885 | for (var _iterator = whitelist[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 886 | var el = _step.value; 887 | 888 | if (selector.includes(el)) return true; 889 | } 890 | } catch (err) { 891 | _didIteratorError = true; 892 | _iteratorError = err; 893 | } finally { 894 | try { 895 | if (!_iteratorNormalCompletion && _iterator.return) { 896 | _iterator.return(); 897 | } 898 | } finally { 899 | if (_didIteratorError) { 900 | throw _iteratorError; 901 | } 902 | } 903 | } 904 | 905 | return false; 906 | }; 907 | 908 | var SelectorFilter = function () { 909 | function SelectorFilter(contentWords, whitelist) { 910 | classCallCheck(this, SelectorFilter); 911 | 912 | this.contentWords = contentWords; 913 | this.rejectedSelectors = []; 914 | this.wildcardWhitelist = []; 915 | this.parseWhitelist(whitelist); 916 | } 917 | 918 | createClass(SelectorFilter, [{ 919 | key: "initialize", 920 | value: function initialize(CssSyntaxTree) { 921 | CssSyntaxTree.on("readRule", this.parseRule.bind(this)); 922 | } 923 | }, { 924 | key: "parseWhitelist", 925 | value: function parseWhitelist(whitelist) { 926 | var _this = this; 927 | 928 | whitelist.forEach(function (whitelistSelector) { 929 | whitelistSelector = whitelistSelector.toLowerCase(); 930 | 931 | if (isWildcardWhitelistSelector(whitelistSelector)) { 932 | // If '*button*' then push 'button' onto list. 933 | _this.wildcardWhitelist.push(whitelistSelector.substr(1, whitelistSelector.length - 2)); 934 | } else { 935 | getAllWordsInSelector(whitelistSelector).forEach(function (word) { 936 | _this.contentWords[word] = true; 937 | }); 938 | } 939 | }); 940 | } 941 | }, { 942 | key: "parseRule", 943 | value: function parseRule(selectors, rule) { 944 | rule.selectors = this.filterSelectors(selectors); 945 | } 946 | }, { 947 | key: "filterSelectors", 948 | value: function filterSelectors(selectors) { 949 | var contentWords = this.contentWords, 950 | rejectedSelectors = this.rejectedSelectors, 951 | wildcardWhitelist = this.wildcardWhitelist, 952 | usedSelectors = []; 953 | 954 | // console.log(selectors) 955 | selectors.forEach(function (selector) { 956 | if (hasWhitelistMatch(selector, wildcardWhitelist)) { 957 | usedSelectors.push(selector); 958 | return; 959 | } 960 | var words = getAllWordsInSelector(selector), 961 | usedWords = words.filter(function (word) { 962 | return contentWords[word.replace('\\', '')]; 963 | }); 964 | 965 | // console.log(words, usedWords, selector) 966 | if (usedWords.length === words.length) { 967 | usedSelectors.push(selector); 968 | } else { 969 | rejectedSelectors.push(selector); 970 | } 971 | }); 972 | 973 | return usedSelectors; 974 | } 975 | }]); 976 | return SelectorFilter; 977 | }(); 978 | 979 | var fs = require("fs"); 980 | var OPTIONS = { 981 | output: false, 982 | minify: false, 983 | info: false, 984 | rejected: false, 985 | whitelist: [], 986 | cleanCssOptions: {} 987 | }; 988 | 989 | var getOptions = function getOptions() { 990 | var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 991 | 992 | var opt = {}; 993 | for (var option in OPTIONS) { 994 | opt[option] = options[option] || OPTIONS[option]; 995 | } 996 | return opt; 997 | }; 998 | 999 | var minify = function minify(cssSource, options) { 1000 | return new CleanCss(options).minify(cssSource).styles; 1001 | }; 1002 | 1003 | var purify = function purify(searchThrough, css, options, callback) { 1004 | if (typeof options === "function") { 1005 | callback = options; 1006 | options = {}; 1007 | } 1008 | options = getOptions(options); 1009 | var cssString = FileUtil.filesToSource(css, "css"), 1010 | content = FileUtil.filesToSource(searchThrough, "content"); 1011 | PrintUtil.startLog(minify(cssString).length); 1012 | var wordsInContent = getAllWordsInContent(content), 1013 | selectorFilter = new SelectorFilter(wordsInContent, options.whitelist), 1014 | tree = new CssTreeWalker(cssString, [selectorFilter]); 1015 | tree.beginReading(); 1016 | var source = tree.toString(); 1017 | 1018 | source = options.minify ? minify(source, options.cleanCssOptions) : source; 1019 | 1020 | // Option info = true 1021 | if (options.info) { 1022 | if (options.minify) { 1023 | PrintUtil.printInfo(source.length); 1024 | } else { 1025 | PrintUtil.printInfo(minify(source, options.cleanCssOptions).length); 1026 | } 1027 | } 1028 | 1029 | // Option rejected = true 1030 | if (options.rejected && selectorFilter.rejectedSelectors.length) { 1031 | PrintUtil.printRejected(selectorFilter.rejectedSelectors); 1032 | } 1033 | 1034 | if (options.output) { 1035 | fs.writeFile(options.output, source, function (err) { 1036 | if (err) return err; 1037 | }); 1038 | } else { 1039 | return callback ? callback(source) : source; 1040 | } 1041 | }; 1042 | 1043 | module.exports = purify; 1044 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purifycss-extended", 3 | "version": "1.3.6", 4 | "description": "Removed unused CSS. Compatible with single-page apps.", 5 | "main": "./lib/purifycss.js", 6 | "module": "./lib/purifycss.es.js", 7 | "jsnext:main": "./lib/purifycss.es.js", 8 | "dependencies": { 9 | "clean-css": "^4.0.12", 10 | "glob": "^7.1.1", 11 | "rework": "^1.0.1", 12 | "uglify-js": "^3.0.6", 13 | "yargs": "^8.0.1" 14 | }, 15 | "devDependencies": { 16 | "babel-plugin-external-helpers": "^6.22.0", 17 | "babel-preset-es2015": "^6.24.1", 18 | "jest": "^19.0.2", 19 | "rollup": "^0.41.6", 20 | "rollup-plugin-babel": "^2.7.1", 21 | "rollup-plugin-commonjs": "^8.0.2", 22 | "rollup-plugin-node-builtins": "^2.1.0", 23 | "rollup-plugin-node-resolve": "^3.0.0", 24 | "rollup-watch": "^3.2.2" 25 | }, 26 | "scripts": { 27 | "test": "jest index.js", 28 | "dev": "rollup -c config/rollup.config.js -w", 29 | "build": "rollup -c config/rollup.config.js" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/HapLifeMan/purifycss-extended.git" 34 | }, 35 | "keywords": [ 36 | "purify", 37 | "extended", 38 | "optimize", 39 | "css", 40 | "remove", 41 | "unused" 42 | ], 43 | "author": "Kenny Tran, Matthew Rourke, Phoebe Li, Thomas Reichling", 44 | "license": "MIT", 45 | "bugs": { 46 | "url": "https://github.com/HapLifeMan/purifycss-extended/issues" 47 | }, 48 | "bin": { 49 | "purifycss": "./bin/purifycss" 50 | }, 51 | "homepage": "https://github.com/HapLifeMan/purifycss-extended" 52 | } 53 | -------------------------------------------------------------------------------- /purified.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 600px) { 2 | div.media-class { 3 | color: black; 4 | } 5 | 6 | .alone { 7 | color: black; 8 | } 9 | 10 | #id-in-media { 11 | color: black; 12 | } 13 | 14 | body { 15 | color: black; 16 | } 17 | } 18 | 19 | @media (max-width: 960px) { 20 | .alone { 21 | color: black; 22 | } 23 | 24 | *, :before, :after { 25 | background: black; 26 | } 27 | } -------------------------------------------------------------------------------- /src/CssTreeWalker.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events" 2 | import rework from "rework" 3 | 4 | const RULE_TYPE = "rule" 5 | const MEDIA_TYPE = "media" 6 | 7 | class CssTreeWalker extends EventEmitter { 8 | constructor(code, plugins) { 9 | super() 10 | this.startingSource = code 11 | this.ast = null 12 | plugins.forEach(plugin => { 13 | plugin.initialize(this) 14 | }) 15 | } 16 | 17 | beginReading() { 18 | this.ast = rework(this.startingSource).use(this.readPlugin.bind(this)) 19 | } 20 | 21 | readPlugin(tree) { 22 | this.readRules(tree.rules) 23 | this.removeEmptyRules(tree.rules) 24 | } 25 | 26 | readRules(rules) { 27 | for (let rule of rules) { 28 | if (rule.type === RULE_TYPE) { 29 | this.emit("readRule", rule.selectors, rule) 30 | } 31 | if (rule.type === MEDIA_TYPE) { 32 | this.readRules(rule.rules) 33 | } 34 | } 35 | } 36 | 37 | removeEmptyRules(rules) { 38 | let emptyRules = [] 39 | 40 | for (let rule of rules) { 41 | const ruleType = rule.type 42 | 43 | if (ruleType === RULE_TYPE && rule.selectors.length === 0) { 44 | emptyRules.push(rule) 45 | } 46 | if (ruleType === MEDIA_TYPE) { 47 | this.removeEmptyRules(rule.rules) 48 | if (rule.rules.length === 0) { 49 | emptyRules.push(rule) 50 | } 51 | } 52 | } 53 | 54 | emptyRules.forEach(emptyRule => { 55 | const index = rules.indexOf(emptyRule) 56 | rules.splice(index, 1) 57 | }) 58 | } 59 | 60 | toString() { 61 | if (this.ast) { 62 | return this.ast.toString().replace(/,\n/g, ",") 63 | } 64 | return "" 65 | } 66 | } 67 | 68 | export default CssTreeWalker 69 | -------------------------------------------------------------------------------- /src/SelectorFilter.js: -------------------------------------------------------------------------------- 1 | import { getAllWordsInSelector } from "./utils/ExtractWordsUtil" 2 | 3 | const isWildcardWhitelistSelector = selector => { 4 | return selector[0] === "*" && selector[selector.length - 1] === "*" 5 | } 6 | 7 | const hasWhitelistMatch = (selector, whitelist) => { 8 | for (let el of whitelist) { 9 | if (selector.includes(el)) return true 10 | } 11 | return false 12 | } 13 | 14 | class SelectorFilter { 15 | constructor(contentWords, whitelist) { 16 | this.contentWords = contentWords 17 | this.rejectedSelectors = [] 18 | this.wildcardWhitelist = [] 19 | this.parseWhitelist(whitelist) 20 | } 21 | 22 | initialize(CssSyntaxTree) { 23 | CssSyntaxTree.on("readRule", this.parseRule.bind(this)) 24 | } 25 | 26 | parseWhitelist(whitelist) { 27 | whitelist.forEach(whitelistSelector => { 28 | whitelistSelector = whitelistSelector.toLowerCase() 29 | 30 | if (isWildcardWhitelistSelector(whitelistSelector)) { 31 | // If '*button*' then push 'button' onto list. 32 | this.wildcardWhitelist.push( 33 | whitelistSelector.substr(1, whitelistSelector.length - 2) 34 | ) 35 | } else { 36 | getAllWordsInSelector(whitelistSelector).forEach(word => { 37 | this.contentWords[word] = true 38 | }) 39 | } 40 | }) 41 | } 42 | 43 | parseRule(selectors, rule) { 44 | rule.selectors = this.filterSelectors(selectors) 45 | } 46 | 47 | filterSelectors(selectors) { 48 | let contentWords = this.contentWords, 49 | rejectedSelectors = this.rejectedSelectors, 50 | wildcardWhitelist = this.wildcardWhitelist, 51 | usedSelectors = [] 52 | 53 | // console.log(selectors) 54 | selectors.forEach(selector => { 55 | if (hasWhitelistMatch(selector, wildcardWhitelist)) { 56 | usedSelectors.push(selector) 57 | return 58 | } 59 | let words = getAllWordsInSelector(selector), 60 | usedWords = words.filter(word => contentWords[word.replace('\\', '')]) 61 | 62 | // console.log(words, usedWords, selector) 63 | if (usedWords.length === words.length) { 64 | usedSelectors.push(selector) 65 | } else { 66 | rejectedSelectors.push(selector) 67 | } 68 | }) 69 | 70 | return usedSelectors 71 | } 72 | } 73 | 74 | export default SelectorFilter 75 | -------------------------------------------------------------------------------- /src/purifycss.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | import CleanCss from "clean-css" 3 | import CssTreeWalker from "./CssTreeWalker" 4 | import FileUtil from "./utils/FileUtil" 5 | import PrintUtil from "./utils/PrintUtil" 6 | import SelectorFilter from "./SelectorFilter" 7 | import { getAllWordsInContent } from "./utils/ExtractWordsUtil" 8 | 9 | const OPTIONS = { 10 | output: false, 11 | minify: false, 12 | info: false, 13 | rejected: false, 14 | whitelist: [], 15 | cleanCssOptions: {} 16 | } 17 | 18 | const getOptions = (options = {}) => { 19 | let opt = {} 20 | for (let option in OPTIONS) { 21 | opt[option] = options[option] || OPTIONS[option] 22 | } 23 | return opt 24 | } 25 | 26 | 27 | const minify = (cssSource, options) => 28 | new CleanCss(options).minify(cssSource).styles 29 | 30 | const purify = (searchThrough, css, options, callback) => { 31 | if (typeof options === "function") { 32 | callback = options 33 | options = {} 34 | } 35 | options = getOptions(options) 36 | let cssString = FileUtil.filesToSource(css, "css"), 37 | content = FileUtil.filesToSource(searchThrough, "content") 38 | PrintUtil.startLog(minify(cssString).length) 39 | let wordsInContent = getAllWordsInContent(content), 40 | selectorFilter = new SelectorFilter(wordsInContent, options.whitelist), 41 | tree = new CssTreeWalker(cssString, [selectorFilter]) 42 | tree.beginReading() 43 | let source = tree.toString() 44 | 45 | source = options.minify ? minify(source, options.cleanCssOptions) : source 46 | 47 | // Option info = true 48 | if (options.info) { 49 | if (options.minify) { 50 | PrintUtil.printInfo(source.length) 51 | } else { 52 | PrintUtil.printInfo(minify(source, options.cleanCssOptions).length) 53 | } 54 | } 55 | 56 | // Option rejected = true 57 | if (options.rejected && selectorFilter.rejectedSelectors.length) { 58 | PrintUtil.printRejected(selectorFilter.rejectedSelectors) 59 | } 60 | 61 | if (options.output) { 62 | fs.writeFile(options.output, source, err => { 63 | if (err) return err 64 | }) 65 | } else { 66 | return callback ? callback(source) : source 67 | } 68 | } 69 | 70 | export default purify 71 | -------------------------------------------------------------------------------- /src/utils/ExtractWordsUtil.js: -------------------------------------------------------------------------------- 1 | const addWord = (words, word) => { 2 | if (word) words.push(word) 3 | } 4 | 5 | export const getAllWordsInContent = content => { 6 | let used = { 7 | // Always include html and body. 8 | html: true, 9 | body: true 10 | } 11 | const words = content.split(/[^a-zA-Z0-9\\@+]/g) 12 | for (let word of words) { 13 | if(word && word !== "+") { used[word] = true } 14 | } 15 | return used 16 | } 17 | 18 | export const getAllWordsInSelector = selector => { 19 | // Remove attr selectors. "a[href...]"" will become "a". 20 | selector = selector.replace(/\[(.+?)\]/g, "").toLowerCase() 21 | // If complex attr selector (has a bracket in it) just leave 22 | // the selector in. ¯\_(ツ)_/¯ 23 | if (selector.includes("[") || selector.includes("]")) { 24 | return [] 25 | } 26 | let skipNextWord = false, 27 | word = "", 28 | words = [] 29 | 30 | let last_letter = ''; 31 | for (let letter of selector) { 32 | if (skipNextWord && !(/[ #.]/).test(letter)) continue 33 | // If pseudoclass or universal selector, skip the next word 34 | if (/[:*]/.test(letter) && last_letter != '\\') { 35 | addWord(words, word.replace('\\', '')) 36 | word = "" 37 | skipNextWord = true 38 | continue 39 | } 40 | if (/[a-zA-Z0-9\\@+]/.test(letter)) { 41 | word += letter 42 | last_letter = letter 43 | } else { 44 | addWord(words, word.replace('\\', '')) 45 | word = "" 46 | skipNextWord = false 47 | } 48 | } 49 | 50 | addWord(words, word) 51 | return words 52 | } 53 | -------------------------------------------------------------------------------- /src/utils/FileUtil.js: -------------------------------------------------------------------------------- 1 | const UglifyJS = require("uglify-js") 2 | const fs = require("fs") 3 | import glob from "glob" 4 | 5 | const compressCode = code => { 6 | try { 7 | // Try to minimize the code as much as possible, removing noise. 8 | let ast = UglifyJS.parse(code) 9 | ast.figure_out_scope() 10 | let compressor = UglifyJS.Compressor({ warnings: false }) 11 | ast = ast.transform(compressor) 12 | ast.figure_out_scope() 13 | ast.compute_char_frequency() 14 | ast.mangle_names({ toplevel: true }) 15 | code = ast.print_to_string().toLowerCase() 16 | } catch (e) { 17 | // If compression fails, assume it's not a JS file and return the full code. 18 | } 19 | return code.toLowerCase() 20 | } 21 | 22 | export const concatFiles = (files, options) => 23 | files.reduce((total, file) => { 24 | let code = "" 25 | try { 26 | code = fs.readFileSync(file, "utf8") 27 | code = options.compress ? compressCode(code) : code 28 | } catch (e) { 29 | console.warn(e.message) 30 | } 31 | return `${total}${code} ` 32 | }, "") 33 | 34 | 35 | export const getFilesFromPatternArray = fileArray => { 36 | let sourceFiles = {} 37 | for (let string of fileArray) { 38 | try { 39 | // See if string is a filepath, not a file pattern. 40 | fs.statSync(string) 41 | sourceFiles[string] = true 42 | } catch (e) { 43 | const files = glob.sync(string) 44 | files.forEach(file => { 45 | sourceFiles[file] = true 46 | }) 47 | } 48 | } 49 | return Object.keys(sourceFiles) 50 | } 51 | 52 | export const filesToSource = (files, type) => { 53 | const isContent = type === "content" 54 | const options = { compress: isContent } 55 | if (Array.isArray(files)) { 56 | files = getFilesFromPatternArray(files) 57 | return concatFiles(files, options) 58 | } 59 | // 'files' is already a source string. 60 | return isContent ? compressCode(files) : files 61 | } 62 | 63 | export default { 64 | concatFiles, 65 | filesToSource, 66 | getFilesFromPatternArray 67 | } 68 | -------------------------------------------------------------------------------- /src/utils/PrintUtil.js: -------------------------------------------------------------------------------- 1 | let startTime 2 | let beginningLength 3 | 4 | const printInfo = endingLength => { 5 | const sizeReduction = (((beginningLength - endingLength) / beginningLength) * 100).toFixed(1) 6 | console.log(` 7 | ________________________________________________ 8 | | 9 | | PurifyCSS has reduced the file size by ~ ${sizeReduction}% 10 | | 11 | ________________________________________________ 12 | `) 13 | } 14 | 15 | const printRejected = rejectedTwigs => { 16 | console.log(` 17 | ________________________________________________ 18 | | 19 | | PurifyCSS - Rejected selectors: 20 | | ${rejectedTwigs.join("\n |\t")} 21 | | 22 | ________________________________________________ 23 | `) 24 | } 25 | 26 | const startLog = cssLength => { 27 | startTime = new Date() 28 | beginningLength = cssLength 29 | } 30 | 31 | export default { 32 | printInfo, 33 | printRejected, 34 | startLog 35 | } 36 | --------------------------------------------------------------------------------