├── .gitignore ├── src ├── helpers.js └── index.js ├── package.json ├── bin └── lilcss.js └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | test 4 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | function matchesArr (str, regExp) { 2 | let matchedArr = [] 3 | let match 4 | while ((match = regExp.exec(str)) !== null) { 5 | matchedArr.push(match.slice(1)) 6 | } 7 | return matchedArr 8 | } 9 | 10 | function flatten (a, b) { 11 | a = a || [] 12 | return a.concat(b) 13 | } 14 | 15 | function uniq (a) { 16 | return Array.from(new Set(a)) 17 | } 18 | 19 | module.exports = { 20 | matchesArr: matchesArr, 21 | flatten: flatten, 22 | uniq: uniq 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lilcss", 3 | "version": "0.0.0", 4 | "description": "Distill out css bloat by parsing static files for selectors", 5 | "main": "src/index.js", 6 | "bin": { 7 | "lilcss": "bin/lilcss.js" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [ 13 | "css", 14 | "clean", 15 | "unused", 16 | "bloat" 17 | ], 18 | "author": "Jon Gacnik (http://jongacnik.com)", 19 | "license": "MIT", 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/jongacnik/lilcss.git" 23 | }, 24 | "homepage": "https://github.com/jongacnik/lilcss#readme", 25 | "dependencies": { 26 | "get-stdin": "^5.0.1", 27 | "glob-all": "^3.1.0", 28 | "xtend": "^4.0.1", 29 | "yargs": "^6.6.0" 30 | }, 31 | "devDependencies": {} 32 | } 33 | -------------------------------------------------------------------------------- /bin/lilcss.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var lilcss = require('../src') 4 | var getStdin = require('get-stdin') 5 | var yargs = require('yargs') 6 | 7 | var argv = yargs 8 | .usage('Usage: $0 style.css -f [files..]') 9 | .options({ 10 | 'f': { 11 | alias: 'files', 12 | demand: true, 13 | describe: 'source files to parse', 14 | type: 'array' 15 | }, 16 | 'a': { 17 | alias: 'attr', 18 | demand: false, 19 | describe: 'attributes to parse', 20 | type: 'array' 21 | }, 22 | 'i': { 23 | alias: 'ignore', 24 | demand: false, 25 | describe: 'selectors to ignore', 26 | type: 'array' 27 | } 28 | }) 29 | .help() 30 | .argv 31 | 32 | getStdin().then(str => { 33 | var input = str || argv._.slice(0).pop() 34 | var files = argv.f || argv.files 35 | var attr = argv.a || argv.attr 36 | var ignore = argv.i || argv.ignore 37 | 38 | if (!input || !files) { 39 | yargs.showHelp() 40 | return 41 | } 42 | 43 | var opts = {} 44 | if (attr) opts['attr'] = attr 45 | if (ignore) opts['ignore'] = ignore 46 | 47 | var output = lilcss(input, files, opts) 48 | 49 | process.stdout.write(output) 50 | }) 51 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | var glob = require('glob-all') 4 | var x = require('xtend') 5 | 6 | var { matchesArr, flatten, uniq } = require('./helpers') 7 | 8 | module.exports = function (css, src, opts) { 9 | if (!css || !src) return 10 | 11 | opts = x({ 12 | ignore: [], 13 | attr: ['class', 'sm', 'md', 'lg', 'xl'] 14 | }, opts) 15 | 16 | var root = process.cwd() 17 | 18 | var css = fs.existsSync(path.join(root, css)) 19 | ? fs.readFileSync(path.join(root, css), 'utf8') 20 | : String(css) 21 | 22 | var files = glob.sync(src).map(s => { 23 | return fs.readFileSync(path.join(root, s), 'utf8') 24 | }) 25 | 26 | function extractSelectors (files) { 27 | var selectorArr = files.map(file => { 28 | // remove new lines to simplify parsing 29 | var minimized = file.replace(/(\r\n|\n|\r)/gm, ' ').replace(/\s{2,}/gm, ' ') 30 | 31 | // find all attribute-like strings: class="..." or class='...' 32 | var re1 = new RegExp(`(${opts.attr.join('|')})="([^]+?)"`, 'ig') 33 | var re2 = new RegExp(`(${opts.attr.join('|')})='([^]+?)'`, 'ig') 34 | var matches1 = matchesArr(minimized, re1) 35 | var matches2 = matchesArr(minimized, re2) 36 | var matches = matches1.concat(matches2) 37 | 38 | // new matching patterns could be added here 39 | // and the matches arrays would be concat 40 | 41 | return getSelectors(matches) 42 | }).reduce(flatten, []) 43 | 44 | return uniq(selectorArr) 45 | } 46 | 47 | function getSelectors (matches) { 48 | return matches.map(match => { 49 | // match[0] selector 50 | // match[1] values 51 | if (match) { 52 | var attr = match[0] === 'class' ? false : match[0] 53 | var vals = getValues(match[1]) 54 | return vals.map(val => makeSelector(val, attr)) 55 | } 56 | }).reduce(flatten, []) 57 | } 58 | 59 | function getValues (string) { 60 | return string 61 | .replace(/[^a-zA-Z0-9\-\s]/ig, '') 62 | .split(' ') 63 | .filter(v => v) 64 | } 65 | 66 | function makeSelector (prefix, attr) { 67 | return attr 68 | ? '[' + attr + '~="' + prefix + '"]' 69 | : '.' + prefix 70 | } 71 | 72 | // tries to put every declaration on single line 73 | // returns Array of lines of css for filtering 74 | function minimizeCss (css) { 75 | return css 76 | .split('\n') 77 | .map(line => line.trim().replace(/\s+\{/, '{')) 78 | .reduce((a, b) => { 79 | var copy = a.slice(0) 80 | var last = copy.pop() 81 | if (!last) { 82 | copy.push(b) 83 | } else if (/{.+}/.test(last) || last.indexOf('@') >= 0 || last === '}') { 84 | copy.push(last) 85 | copy.push(b) 86 | } else { 87 | copy.push(last + b) 88 | } 89 | return copy 90 | }, []) 91 | } 92 | 93 | function cleanCss (css) { 94 | var minimized = minimizeCss(css) 95 | return minimized.filter(line => { 96 | // skip if not class or attribute selector ¯\_(ツ)_/¯ 97 | if (!/^[\.\[]/i.test(line)) { 98 | return true 99 | } else { 100 | return selectors.some(sel => { 101 | return line.indexOf(sel + '{') >= 0 102 | }) 103 | } 104 | }).join('\n') 105 | } 106 | 107 | var selectors = extractSelectors(files).concat(opts.ignore) 108 | var cleaned = cleanCss(css) 109 | 110 | return cleaned 111 | } 112 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

lilcss ⚗

2 | 3 |
4 | 5 | Stability 6 | 7 | 8 | NPM version 9 | 10 |
11 | 12 |
13 | 14 | Distill out css bloat by parsing static files for selectors 15 | 16 | ## Example 17 | 18 | Let's run `lilcss` via the command line, passing in our css file containing a bunch of unused styles, and all of our site templates. We'll pipe the results to a new file. 19 | 20 | ``` 21 | $ lilcss fat.css -f templates/*.js > lil.css 22 | ``` 23 | 24 |
25 | fat.css 26 | 27 | ```css 28 | .c1{width:8.333333333333332%} 29 | .c2{width:16.666666666666664%} 30 | .c3{width:25%} 31 | .c4{width:33.33333333333333%} 32 | .c5{width:41.66666666666667%} 33 | .c6{width:50%} 34 | .c7{width:58.333333333333336%} 35 | .c8{width:66.66666666666666%} 36 | .c9{width:75%} 37 | .c10{width:83.33333333333334%} 38 | .c11{width:91.66666666666666%} 39 | .c12{width:100%} 40 | .df{display:flex} 41 | .db{display:block} 42 | .dib{display:inline-block} 43 | .di{display:inline} 44 | .dt{display:table} 45 | .dtc{display:table-cell} 46 | .dtr{display:table-row} 47 | .dn{display:none} 48 | ``` 49 | 50 |
51 | 52 |
53 | templates 54 | 55 | ```js 56 | module.exports = html` 57 |
58 | ` 59 | ``` 60 | 61 | ```js 62 | module.exports = html` 63 |
64 |
65 | ` 66 | ``` 67 | 68 |
69 | 70 | Our new file will now only contain the css we need: 71 | 72 | ```css 73 | .c2{width:16.666666666666664%} 74 | .c4{width:33.33333333333333%} 75 | .c8{width:66.66666666666666%} 76 | .db{display:block} 77 | .dib{display:inline-block} 78 | .dn{display:none} 79 | ``` 80 | 81 | ## API 82 | 83 | #### `lilcss(css, src, options)` 84 | 85 | Returns the distilled css. 86 | 87 | | option | default | controls | 88 | | --- | --- | --- | 89 | | ignore | `[]` | any selectors to ignore | 90 | | attr | `['class', 'sm', 'md', 'lg', 'xl']` | attributes to parse from files | 91 | 92 | ## Notes 93 | 94 | `lilcss` aims to solve similar problems as [uncss](https://github.com/giakki/uncss) and [purifycss](https://github.com/purifycss/purifycss) but is a *much* less general solution. To minimize complexity, it is assumed: 95 | 96 | 1. A single css file will be parsed 97 | 2. The css file must not be minified prior to being parsed 98 | 3. General selectors, such as `body` or `input` will always be preserved 99 | 4. Any attribute selector will be treated as `~=` 100 | 5. Only class and attribute selectors are supported 101 | 6. Only HTML-like attributes will be parsed, things like `classList.add()` are not supported: 102 | 103 | Input template to parse: 104 | ```html 105 |
106 | ``` 107 | 108 | Extracted selectors: 109 | ```js 110 | ['.c1', '.dn', '[sm~="c2"]'] 111 | ``` 112 | 113 | Anything which does not match these selectors will be removed from the css. 114 | 115 | ## Why? 116 | 117 | Removing unused css output is important but the existing tools don't work well for my needs. [uncss](https://github.com/giakki/uncss) requires code to be run in a headless browser but this assumes too much about *how* you are building your site. [purifycss](https://github.com/purifycss/purifycss) does a bit better in that it parses static files, but the results are generally unpredictable and attribute selectors are fully unsupported. 118 | 119 | `lilcss` has specific requirements but this allows the code to stay small and concise. More functionality may be introduced as needed. 120 | 121 | ## See Also 122 | 123 | - [gr8](https://github.com/jongacnik/gr8) 124 | - [gr8-util](https://github.com/jongacnik/gr8-util) 125 | - [quick-styles](https://github.com/jongacnik/quick-styles) 126 | 127 | ## Todo 128 | 129 | - [ ] Tests 130 | - [ ] Better regex (or perhaps ditch and ast this thing) 131 | --------------------------------------------------------------------------------