├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── lib ├── processors │ ├── css.js │ ├── html.js │ ├── js-strings.js │ └── remove-unused.js └── utils │ ├── expressions.js │ ├── generate-shortname.js │ ├── library.js │ └── processor-utils.js ├── package.json └── test ├── example ├── gulpfile.js ├── index.html ├── script.js └── style.css ├── expressions ├── expressions.class-selector.test.js ├── expressions.element-attribute.test.js ├── expressions.id-selector.test.js ├── expressions.js-string.test.js └── expressions.selector-name.test.js ├── generate-shortname.test.js ├── library.test.js ├── processor-utils.test.js └── processors ├── css.test.js └── html.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | coverage -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | .travis.yml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | after_script: 5 | - npm run test-report -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Caleb Brewer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This project is looking for a maintainer 2 | I am no longer maintaining this repository and would be happy to transfer it to someone who still actively uses it. Respond to the [maintainer issue](https://github.com/cazzer/gulp-selectors/issues/26) if you are interested in taking ownership. In case you are interested, I use [css-modules](https://github.com/css-modules/css-modules) now instead. 3 | 4 | # gulp-selectors 5 | [![Build Status][travis-image]][travis-url] [![Code Climate][cc-image]][cc-url] [![Test Coverage][coverage-image]][coverage-url] [![NPM Version][npm-image]][npm-url] 6 | 7 | > Minify those pesky selector names down to nothing with this fancy gulp plugin. Minified selectors will be applied consistently across all files piped into it. 8 | 9 | Input | Output 10 | ----------------------------------------|---------- 11 | `.class-name { ... }` |`.a { ... }` 12 | `.another-class { ... }` |`.b { ... }` 13 | `#an-id { ... }` |`#a { ... }` 14 | `
...
` |`
...
` 15 | 16 | *You're like: `.some-super-descriptive-selector-name {...}`, and it's like: `.a {...}`* 17 | 18 | ## Usage 19 | 20 | First and foremost: 21 | `npm install gulp-selectors` 22 | 23 | ```js 24 | var gulp = require('gulp'); 25 | var gs = require('gulp-selectors'); 26 | 27 | gulp.src(['src/**/*.css', 'src/**/*.html']) 28 | .pipe(gs.run()) 29 | .pipe(gulp.dest('dist')); 30 | ``` 31 | 32 | You can also pass some options into run: 33 | 34 | ` gs.run(processors, ignores)` 35 | 36 | CSS and HTML files are processed well by default, just pass in your glob of files and all classes and IDs will be reduced to a minified form. Of course you can use it for some more specific functions if you like. See the included [sample gulpfile](https://github.com/calebthebrewer/gulp-selectors/blob/master/test/example/gulpfile.js) for a full example of how to effectively use gulp-selectors in your gulp workflow. 37 | 38 | ### Defaults 39 | 40 | All arguments are optional. If omitted, processors will default to `css` and `html` and ignores 41 | will be empty: 42 | 43 | ```js 44 | gs.run({ 45 | 'css': ['css'], 46 | 'html': ['html'] 47 | }, { 48 | }); 49 | ``` 50 | 51 | ### Advanced Usage 52 | 53 | ```js 54 | var processors = { 55 | 'css': ['scss', 'css'], // run the css processor on .scss and .css files 56 | 'html': ['haml'], // run the html processor on .haml files 57 | 'js-strings': ['js'] // run the js-strings plugin on js files 58 | }, 59 | ignores = { 60 | classes: ['hidden', 'active'] // ignore these class selectors, 61 | ids: '*' // ignore all IDs 62 | }; 63 | 64 | gs.run(processors, ignores); 65 | ``` 66 | 67 | Two processors are built in for your convenience: `css` and `html` are stable but `js-strings` and `remove-unused` are beta and may be moved to their own repositories. 68 | 69 | - css: matches .selectors and #selectors 70 | - html: matches id="selector"s, class="selector"s, and for="selector"s 71 | - js: matches exact strings by looping through the library, which is dangerous if you use common words as selectors 72 | - remove-unused: should be run last, and only on stylesheets - it removes all declarations present in the library which haven't been used 73 | 74 | If a processor is listed which isn't built in, gulp-selectors will attempt to `require` it. 75 | 76 | ## How gulp-selectors works 77 | 78 | Calling `gs.run()` builds a library which persists for all processors used in the call. Processors are run on all associated files and all selectors, besides those that have been ignored, will be minified. 79 | 80 | ### Processors 81 | 82 | ```js 83 | { 84 | 'css': ['css', 'scss'], 85 | 'html': ['html', 'tpl.js'], 86 | 'js-strings': ['js', '!tpl.js'], 87 | 'your-custom-processor': ['.ext'] 88 | } 89 | ``` 90 | 91 | `css` and `html` are built in. Additional processors referenced will be injected where needed so it is important to ensure all are installed. Processors are used like this: 92 | 93 | ```js 94 | processor(file, classLibrary, idLibrary) 95 | ``` 96 | 97 | `File` is the string containing the file contents. Each of the two libraries exposes the following API: 98 | 99 | - set(selectorName): returns a minified selector name 100 | - has(selectorName): tests if the name exists 101 | - get(selectorName, [dontCount]): ... 102 | 103 | ```js 104 | libraries 105 | ``` 106 | 107 | ### Ignores 108 | 109 | ```js 110 | { 111 | ids: ['content', 'target'], 112 | classes: ['hidden', 'active'] 113 | } 114 | ``` 115 | 116 | 117 | [travis-url]: https://travis-ci.org/calebthebrewer/gulp-selectors 118 | [travis-image]: https://travis-ci.org/calebthebrewer/gulp-selectors.svg?branch=master 119 | [cc-image]: https://codeclimate.com/github/calebthebrewer/gulp-selectors/badges/gpa.svg 120 | [cc-url]: https://codeclimate.com/github/calebthebrewer/gulp-selectors 121 | [coverage-image]: https://codeclimate.com/github/calebthebrewer/gulp-selectors/badges/coverage.svg 122 | [coverage-url]: https://codeclimate.com/github/calebthebrewer/gulp-selectors 123 | [npm-image]: https://badge.fury.io/js/gulp-selectors.svg 124 | [npm-url]: http://badge.fury.io/js/gulp-selectors 125 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var es = require('event-stream'), 3 | _ = require('lodash'), 4 | utils = require('gulp-util'), 5 | processorUtils = require('./lib/utils/processor-utils'), 6 | Library = require('./lib/utils/library'); 7 | 8 | var processor; 9 | 10 | module.exports = { 11 | run: run, 12 | minify: run, 13 | info: info, 14 | create: create 15 | }; 16 | 17 | class Processor { 18 | constructor(processors, ignores) { 19 | this.ignores = _.extend({classes: [], ids: []}, ignores); 20 | 21 | //ensure processor names are set as expected 22 | this.processors = processorUtils.extendDefaults(processors); 23 | 24 | //build new libraries to use 25 | this.classLibrary = new Library(this.ignores.classes || []); 26 | this.idLibrary = new Library(this.ignores.ids || []); 27 | } 28 | 29 | run() { 30 | /** 31 | * Main task for mini selectors uglify classes. Processes files based on type. 32 | * 33 | * @param file Stream from es.map 34 | * @param callback for es.map 35 | */ 36 | var miniSelectors = (file, callback) => { 37 | var extensions = file.path.split('.'), 38 | extension = extensions[extensions.length - 1], 39 | reducedFile = String(file.contents); 40 | 41 | processorUtils.getForExtension(this.processors, extension).forEach((processor) => { 42 | reducedFile = processor(reducedFile, this.classLibrary, this.idLibrary); 43 | }); 44 | 45 | file.contents = new Buffer(reducedFile); 46 | callback(null, file); 47 | } 48 | 49 | return es.map(miniSelectors); 50 | } 51 | 52 | info() { 53 | return es.map((file, callback) => { 54 | utils.log(file.history[0]); 55 | utils.log('Class library:', this.classLibrary.stats()); 56 | utils.log('ID library:', this.idLibrary.stats()); 57 | callback(null, file); 58 | }); 59 | } 60 | } 61 | 62 | function run(processors, ignores) { 63 | processor = new Processor(processors, ignores); 64 | return processor.run(); 65 | } 66 | 67 | function info() { 68 | return processor.info(); 69 | } 70 | 71 | function create(processors, ignores) { 72 | return new Processor(processors, ignores); 73 | } 74 | -------------------------------------------------------------------------------- /lib/processors/css.js: -------------------------------------------------------------------------------- 1 | var expressions = require('../utils/expressions'); 2 | 3 | /** 4 | * Replaces all class names with shortnames. Also builds a library of shortnames which can be 5 | * used to reduce other file types. 6 | * 7 | * @param {string} File 8 | * @returns {string} Minified file 9 | */ 10 | module.exports = function(file, classLibrary, idLibrary) { 11 | var selectorNameMatch = expressions.selectorName; 12 | 13 | file = file.replace(expressions.classSelector, function(selector) { 14 | //exclude property values (matches ending in ')') 15 | if (selector[selector.length - 1] === ')') { 16 | return selector; 17 | } 18 | return selector.replace(selectorNameMatch, function(selectorName) { 19 | return classLibrary.get(selectorName, true); 20 | }); 21 | }); 22 | 23 | file = file.replace(expressions.idSelector, function(selector) { 24 | //exclude property values (matches ending in '; or }') 25 | if (selector[selector.length - 1] === ';' || selector[selector.length - 1] === '}') { 26 | return selector; 27 | } 28 | return selector.replace(selectorNameMatch, function(selectorName) { 29 | return idLibrary.get(selectorName, true); 30 | }); 31 | }); 32 | 33 | return file; 34 | }; -------------------------------------------------------------------------------- /lib/processors/html.js: -------------------------------------------------------------------------------- 1 | var expressions = require('../utils/expressions'); 2 | 3 | /** 4 | * Replaces all class and id attributes found in the library. Only tested on *.html files with 5 | * classes declared in *class= attributes. 6 | * 7 | * @param {string} File 8 | * @returns {string} Minified file 9 | */ 10 | module.exports = function (file, classLibrary, idLibrary) { 11 | return file.replace(expressions.elementAttribute, function(attributes) { 12 | var attribute = attributes.split('='); 13 | return attribute[0] + '=' + attribute[1] 14 | .replace(expressions.selectorName, function(selectorName) { 15 | switch (attribute[0]) { 16 | case 'id': 17 | case 'for': 18 | return idLibrary.get(selectorName); 19 | default: //class 20 | return classLibrary.get(selectorName); 21 | } 22 | }); 23 | }); 24 | }; -------------------------------------------------------------------------------- /lib/processors/js-strings.js: -------------------------------------------------------------------------------- 1 | var expressions = require('../utils/expressions'); 2 | 3 | /** 4 | * Dumb search for all strings in all JS files. This will only work on libraries which are fully built. 5 | * 6 | * @param file String 7 | * @returns {reducedFile String} 8 | */ 9 | module.exports = function(file, classLibrary, idLibrary) { 10 | 11 | classLibrary.getFullNames().forEach(function(selector) { 12 | file = file.replace(expressions.jsString(selector), function() { 13 | return "'" + classLibrary.get(selector) + "'"; 14 | }); 15 | }); 16 | 17 | idLibrary.getFullNames().forEach(function(selector) { 18 | file = file.replace(expressions.jsString(selector), function() { 19 | return "'" + idLibrary.get(selector) + "'"; 20 | }); 21 | }); 22 | 23 | return file; 24 | }; -------------------------------------------------------------------------------- /lib/processors/remove-unused.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Removes all CSS declarations in the library which are unused. 3 | * This doesn't get anything with a secondary selector. Help my regex! 4 | * 5 | * @param file 6 | * @param classLibrary 7 | * @returns {*} 8 | */ 9 | module.exports = function(file, classLibrary) { 10 | 11 | classLibrary.getUnused().forEach(function(selector) { 12 | var expression = new RegExp('[,?\\s*]\\.' + selector + '\\s*{[^}]*}', 'g'); 13 | file = file.replace(expression, function(match) { 14 | if (match[0] !== ',') { 15 | //remove declaration 16 | return ''; 17 | } else { 18 | //only remove selector 19 | var subExpression = new RegExp('[,?\\s*]\\.' + selector); 20 | return match.replace(subExpression, function() { 21 | return ''; 22 | }); 23 | } 24 | }); 25 | }); 26 | 27 | return file; 28 | }; -------------------------------------------------------------------------------- /lib/utils/expressions.js: -------------------------------------------------------------------------------- 1 | var selectorName = '(-?[_a-zA-Z]+[_\\w-]*)'; 2 | var selectorSuffix = '((?=[\\s:#\\.\\{,\\>\\~+\\[\\]]))(?=(?:.|\n|\r)*{)'; 3 | 4 | /** 5 | * Some awesome expressions that I'll explain in a bit. 6 | * Test these dope boats live on http://scriptular.com/ to make your life easier. 7 | */ 8 | module.exports = { 9 | /** 10 | * Selector name extracts valid selector names from selectors. Basically we use the selector 11 | * expression to find the selectors, then use this to extract the actual name which need to be 12 | * shortened. 13 | * 14 | * Should match: 15 | * - selector 16 | * - -selector 17 | * - _selector 18 | * - selector0 19 | * 20 | * Shouldn't match: 21 | * - 3selector 22 | * - :selector 23 | */ 24 | selectorName: new RegExp(selectorName, 'g'), 25 | /** 26 | * Selectors should match any classes and ids defined in a stylesheet. 27 | * 28 | * NOTE: This will also match hex values but will capture the close ; or } so that it can be 29 | * ignored later. 30 | * 31 | * Should match: 32 | * - #selector 33 | * - .selector 34 | * - #e6e6e6; 35 | * 36 | * Shouldn't match: 37 | * - body 38 | * - #666 39 | * - :pseudo 40 | * - url('foobar.classname') 41 | * - other stupid stuff...I dunno check the tests 42 | */ 43 | classSelector: new RegExp('(\\.|\\[class[\\^\\$\\|\\*]?=)' + selectorName + selectorSuffix, 'gi'), 44 | idSelector: new RegExp('(#|\\[id[\\^\\$\\|\\*]?=)' + selectorName + selectorSuffix, 'gi'), 45 | /** 46 | * Matches HTML class, id, and for attributes. I think those are the only ones we care about... 47 | * 48 | * NOTE: This expression should also match attributes with spaces between the assignment, e.g. 49 | * 50 | * Should match: 51 | * - class="selector" 52 | * - class="selector selector" 53 | * - id="selector" 54 | * - for="selector" 55 | * 56 | * Shouldn't match: 57 | * - name="selector" 58 | * - href="selector" 59 | */ 60 | elementAttribute: /(class|id|for|aria-labelledby)\s*=\s*["'](-?[_a-zA-Z]+[_\w-\s]*)["']/g, 61 | /** 62 | * Matches ID Values 63 | * 64 | * All values can be single values or lists and lists can be either comma separated (each val is surrounded by single or double quotes OR a space separated list with not internal quotes (only quotes at beginning and end). 65 | * .getElementById 66 | * .id 67 | * $ Only matches values with "#" 68 | * jQuery Only matches values with "#" 69 | * .attr This has a known bug. There is no way to differentiate the leading "class" from "id" in attr without look-behind, which javascript does not support. Therefore, all matches using this regex need to be filtered to remove results which are lists that have "class" as the first value. 70 | */ 71 | idList: /(?:\$|jQuery|(?:\.(?:getElementById|id|jQuery|attr)))\s*[\(=]{1}\s*(["']{1}#?-?[_a-zA-Z]+[_\w-]*(?:(?:(?:["']{1}\s*,\s*["']{1})|\s+){1}#?-?[_a-zA-Z]+[_\w-]*)*["']{1})/, 72 | /** 73 | * Matches Class Values 74 | * 75 | * All values can be single values or lists and lists can be either comma separated (each val is surrounded by single or double quotes OR a space separated list with not internal quotes (only quotes at beginning and end). 76 | * .getElementsByClassName 77 | * .classList.add 78 | * .classList.remove 79 | * .className 80 | * $ Only matches values with "." 81 | * jQuery Only matches values with "." 82 | * .addClass 83 | * .toggleClass 84 | * .removeClass 85 | * .attr This has a known bug. There is no way to differentiate the leading "class" from "id" in attr without look-behind, which javascript does not support. Therefore, all matches using this regex need to be filtered to remove results which are lists that have "id" as the first value. 86 | * .hasClass 87 | */ 88 | classList: /(?:\$|jQuery|(?:\.(?:getElementsByClassName|classList\.add|classList\.remove|className|jQuery|addClass|toggleClass|removeClass|attr|hasClass)))\s*[\(=]{1}\s*(["']{1}\.?-?[_a-zA-Z]+[_\w-]*(?:(?:(?:["']{1}\s*,\s*["']{1})|\s+){1}\.?-?[_a-zA-Z]+[_\w-]*)*["']{1})/, 89 | /** 90 | * Builds a regular expression which will match a quoted string. 91 | * 92 | * @param name String of selector name 93 | * @returns {RegExp} 94 | */ 95 | jsString: function(name) { 96 | return new RegExp('[\'|"]' + name + '[\'|"]', 'g'); 97 | } 98 | }; 99 | -------------------------------------------------------------------------------- /lib/utils/generate-shortname.js: -------------------------------------------------------------------------------- 1 | module.exports = generateShortname; 2 | 3 | /** 4 | * Helper function for generating shortnames based on an alphabetic library. 5 | * 6 | * @param seed Integer 7 | * @returns {string Shortname} 8 | */ 9 | function generateShortname(seed) { 10 | if (seed !== parseInt(seed, 10)) { 11 | throw new Error('Seed must be a number'); 12 | } 13 | 14 | if (seed < 0) { 15 | throw new Error('Seed must be at least 0'); 16 | } 17 | 18 | var library = 'abcdefghijklmnopqrstuvwxyz', 19 | libraryLength = library.length, 20 | prefix = ''; 21 | //break the seed down if it is larger than the library 22 | if (seed >= libraryLength) { 23 | prefix = generateShortname(Math.floor(seed / libraryLength) - 1); 24 | } 25 | //return the prefixed shortname 26 | return prefix + library[seed % libraryLength]; 27 | } -------------------------------------------------------------------------------- /lib/utils/library.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | multimatch = require('multimatch'), 3 | generateShortname = require('./generate-shortname'); 4 | 5 | /** 6 | * Provides libraries. 7 | */ 8 | module.exports = Library; 9 | 10 | /** 11 | * This handy function returns an empty library. This should be a module on its own so that the 12 | * logic of retrieving a library does not get mixed up with the library logic itself. 13 | * 14 | * @returns {object} Library 15 | * @constructor 16 | */ 17 | function Library(ignores) { 18 | var _library = {}, 19 | _ignores = ignores || [], 20 | size = 0; 21 | 22 | /** 23 | * Tests if a value exists in the library. 24 | * 25 | * @params {string} name 26 | * @returns {boolean} Bool for if it exists 27 | */ 28 | this.has = function(name) { 29 | return _library[name] !== undefined; 30 | }; 31 | 32 | /** 33 | * Ensures the name is set and returns it. If generates an ignored name, 34 | * will increase size and try again 35 | * 36 | * @param name String name to get shortname for from the library 37 | * @param dontCount Bool to not to count this as a use in the code 38 | * @returns {string} Shortname of the minified name 39 | */ 40 | this.get = function(name, dontCount) { 41 | //catch all for ignoring IDs 42 | if (ignores === true) return name; 43 | 44 | var shortname; 45 | 46 | if (_library[name]) { 47 | shortname = _library[name].shortname; 48 | if (!dontCount) { 49 | _library[name].hits++; 50 | } 51 | } else if (!multimatch(name, _ignores).length) { 52 | do { 53 | shortname = generateShortname(size); 54 | size++; 55 | } while (~_ignores.indexOf(shortname)); 56 | 57 | _library[name] = { 58 | shortname: shortname, 59 | hits: dontCount ? 0 : 1 60 | }; 61 | } else { 62 | shortname = name; 63 | } 64 | 65 | return shortname; 66 | }; 67 | 68 | /** 69 | * Returns all of the shortnames in the library. 70 | * Does not count towards usage. 71 | * 72 | * @returns {array} Of all shortnames. 73 | */ 74 | this.getAll = function() { 75 | return _.pluck(_library, 'shortname'); 76 | }; 77 | 78 | /** 79 | * Retrieves shortnames which are not used in the code processed. 80 | * 81 | * @returns {array} Of unused names 82 | */ 83 | this.getUnused = function() { 84 | return _.pluck( 85 | _.filter( 86 | _library, 87 | function(entry) { 88 | return entry.hits === 0; 89 | }), 'shortname'); 90 | }; 91 | 92 | /** 93 | * Returns the size of the library. 94 | * 95 | * @returns {number} Number of entries in the library 96 | */ 97 | this.size = function() { 98 | return size; 99 | }; 100 | 101 | /** 102 | * Returns the full selector names which have been entered so far. 103 | * 104 | * @returns {Array} All full selector names 105 | */ 106 | this.getFullNames = function() { 107 | return Object.keys(_library); 108 | }; 109 | 110 | this.stats = function() { 111 | return { 112 | size: this.size(), 113 | unused: this.getUnused().length 114 | }; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/utils/processor-utils.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | module.exports = { 4 | extendDefaults: extendDefaults, 5 | getForExtension: getProcessorsForExtension 6 | }; 7 | 8 | function extendDefaults(processors) { 9 | 10 | processors = _.extend({ 11 | css: ['css'], 12 | html: ['html'] 13 | }, processors); 14 | 15 | return processors; 16 | } 17 | 18 | function getProcessorsForExtension(processors, extension) { 19 | var selectedProcessors = []; 20 | 21 | for (var processor in processors) { 22 | if (typeof processors[processor] === 'object' && 23 | processors[processor].indexOf(extension) > -1) { 24 | switch (processor) { 25 | //these guys are special since they're built in 26 | case 'css': 27 | case 'html': 28 | case 'js-strings': 29 | case 'remove-unused': 30 | selectedProcessors.push(require('../processors/' + processor)); 31 | break; 32 | default: 33 | selectedProcessors.push(require(processor)); 34 | } 35 | } 36 | } 37 | 38 | return selectedProcessors; 39 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-selectors", 3 | "description": "Minify CSS selectors.", 4 | "version": "0.1.10", 5 | "author": { 6 | "name": "Caleb Brewer", 7 | "email": "calebthebrewer@gmail.com" 8 | }, 9 | "dependencies": { 10 | "event-stream": "~3.1.7", 11 | "gulp-util": "~3.0.1", 12 | "lodash": "~2.4.1", 13 | "multimatch": "^1.0.1" 14 | }, 15 | "homepage": "https://github.com/calebthebrewer/gulp-selectors", 16 | "bugs": "https://github.com/calebthebrewer/gulp-selectors/issues", 17 | "keywords": [ 18 | "gulp", 19 | "gulpplugin", 20 | "munch", 21 | "minify", 22 | "css", 23 | "selectors", 24 | "styles", 25 | "compress" 26 | ], 27 | "license": "MIT", 28 | "main": "index.js", 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/calebthebrewer/gulp-selectors.git" 32 | }, 33 | "readmeFilename": "README.md", 34 | "devDependencies": { 35 | "vows": "~0.7.0", 36 | "jscoverage": "~0.5.6", 37 | "istanbul": "^0.3.2", 38 | "codeclimate-test-reporter": "0.0.4", 39 | "gulp": "~3.8.11" 40 | }, 41 | "scripts": { 42 | "test": "vows --spec", 43 | "coverage": "istanbul cover ./node_modules/vows/bin/vows --spec", 44 | "codeclimate": "cat ./coverage/lcov.info | codeclimate", 45 | "test-report": "npm run coverage && npm run codeclimate" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/example/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var selectors = require('../../index'); 3 | 4 | gulp.task('default', function() { 5 | return gulp.src(['index.html', 'style.css', 'script.js']) 6 | .pipe(selectors.run({ 7 | 'js-strings': ['js'] 8 | })) 9 | .pipe(selectors.info()) 10 | .pipe(gulp.dest('./dist')); 11 | }); -------------------------------------------------------------------------------- /test/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Gulp Selectors Demo 4 | 5 | 6 | 7 |

If things aren't as they say, I'm broken!

8 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/example/script.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | wiggle(document.getElementById('wiggle'), 0); 3 | 4 | function wiggle(element, time) { 5 | element.style.marginLeft = Math.sin(time) * 10; 6 | time += .1; 7 | setTimeout(wiggle.bind(this, element, time), 10); 8 | } 9 | })(); -------------------------------------------------------------------------------- /test/example/style.css: -------------------------------------------------------------------------------- 1 | .blue { 2 | color: blue; 3 | } 4 | 5 | .big { 6 | font-size: 2em; 7 | } -------------------------------------------------------------------------------- /test/expressions/expressions.class-selector.test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'), 2 | assert = require('assert'), 3 | expressions = require('../../lib/utils/expressions'); 4 | 5 | vows.describe('Expressions: class selectors').addBatch({ 6 | 'An class selector': { 7 | topic: '.selector {'.match(expressions.classSelector), 8 | 'should return one match': function(topic) { 9 | assert.equal(topic.length, 1); 10 | } 11 | }, 12 | 'An uncommon class selector': { 13 | topic: '[class=selector] {'.match(expressions.classSelector), 14 | 'should return a match': function(topic) { 15 | assert.equal(topic.length, 1); 16 | } 17 | }, 18 | 'An uncommon extended class selector': { 19 | topic: '[class*=selector] {'.match(expressions.classSelector), 20 | 'should return a match': function(topic) { 21 | assert.equal(topic.length, 1); 22 | } 23 | }, 24 | 'Nested class selectors': { 25 | topic: '.selector .child {'.match(expressions.classSelector), 26 | 'should return two matches': function(topic) { 27 | assert.equal(topic.length, 2); 28 | } 29 | }, 30 | 'An url selector': { 31 | topic: 'url(\'foobar.selector\')'.match(expressions.classSelector), 32 | 'should not return a match': function(topic) { 33 | assert.equal(topic, null); 34 | } 35 | }, 36 | 'A class with a pseudo selector': { 37 | topic: '.selector:hover {'.match(expressions.classSelector), 38 | 'should return one match': function(topic) { 39 | assert.equal(topic.length, 1); 40 | } 41 | }, 42 | 'A pseudo selector': { 43 | topic: ':hover {'.match(expressions.classSelector), 44 | 'should not return a match': function(topic) { 45 | assert.equal(topic, null); 46 | } 47 | }, 48 | 'A tag selector': { 49 | topic: 'body {'.match(expressions.classSelector), 50 | 'should return a match': function(topic) { 51 | assert.equal(topic, null); 52 | } 53 | } 54 | }).export(module); 55 | -------------------------------------------------------------------------------- /test/expressions/expressions.element-attribute.test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'), 2 | assert = require('assert'), 3 | expressions = require('../../lib/utils/expressions'); 4 | 5 | vows.describe('Expressions: element attribute').addBatch({ 6 | 'An id attribute': { 7 | topic: 'id="selector"'.match(expressions.elementAttribute), 8 | 'should return a match': function(topic) { 9 | assert.equal(topic.length, 1); 10 | } 11 | }, 12 | 'A class attribute': { 13 | topic: 'class="selector"'.match(expressions.elementAttribute), 14 | 'should return a match': function(topic) { 15 | assert.equal(topic.length, 1); 16 | } 17 | }, 18 | 'A for attribute': { 19 | topic: 'for="selector"'.match(expressions.elementAttribute), 20 | 'should return a match': function(topic) { 21 | assert.equal(topic.length, 1); 22 | } 23 | }, 24 | 'An href attribute': { 25 | topic: 'href="selector"'.match(expressions.elementAttribute), 26 | 'should not return a match': function(topic) { 27 | assert.equal(topic, null); 28 | } 29 | }, 30 | 'A class attribute with different quotes': { 31 | topic: "class='selector'".match(expressions.elementAttribute), 32 | 'should return a match': function(topic) { 33 | assert.equal(topic.length, 1); 34 | } 35 | }, 36 | 'A class attribute with awkward spacing': { 37 | topic: 'class = "selector"'.match(expressions.elementAttribute), 38 | 'should return a match': function(topic) { 39 | assert.equal(topic.length, 1); 40 | } 41 | }, 42 | 'An aria-labelledby attribute': { 43 | topic: 'aria-labelledby="selector"'.match(expressions.elementAttribute), 44 | 'should return a match': function(topic) { 45 | assert.equal(topic.length, 1); 46 | } 47 | } 48 | }).export(module); -------------------------------------------------------------------------------- /test/expressions/expressions.id-selector.test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'), 2 | assert = require('assert'), 3 | expressions = require('../../lib/utils/expressions'); 4 | 5 | vows.describe('Expressions: selector').addBatch({ 6 | 'An id selector': { 7 | topic: '#selector {'.match(expressions.idSelector), 8 | 'should return a match': function(topic) { 9 | assert.equal(topic.length, 1); 10 | } 11 | }, 12 | 'An uncommon id selector': { 13 | topic: '[id=selector] {'.match(expressions.idSelector), 14 | 'should return a match': function(topic) { 15 | assert.equal(topic.length, 1); 16 | } 17 | }, 18 | 'An uncommon extended id selector': { 19 | topic: '[id*=selector] {'.match(expressions.idSelector), 20 | 'should return a match': function(topic) { 21 | assert.equal(topic.length, 1); 22 | } 23 | }, 24 | 'A compressed id selector': { 25 | topic: '#selector{'.match(expressions.idSelector), 26 | 'should return a match': function(topic) { 27 | assert.equal(topic.length, 1); 28 | } 29 | }, 30 | 'A hash value': { 31 | topic: '#e6e6e6;'.match(expressions.idSelector), 32 | 'should not return a match': function(topic) { 33 | assert.equal(topic, null); 34 | } 35 | }, 36 | 'An id with a pseudo selector': { 37 | topic: '#selector:hover {'.match(expressions.idSelector), 38 | 'should return one match': function(topic) { 39 | assert.equal(topic.length, 1); 40 | } 41 | }, 42 | 'A string beginning with a number': { 43 | topic: '#666 {'.match(expressions.idSelector), 44 | 'should not return a match': function(topic) { 45 | assert.equal(topic, null); 46 | } 47 | }, 48 | 'A string beginning with a weird character': { 49 | topic: '#:selector {'.match(expressions.idSelector), 50 | 'should not return a match': function(topic) { 51 | assert.equal(topic, null); 52 | } 53 | } 54 | }).export(module); 55 | -------------------------------------------------------------------------------- /test/expressions/expressions.js-string.test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'), 2 | assert = require('assert'), 3 | expressions = require('../../lib/utils/expressions'); 4 | 5 | vows.describe('Expressions: js string selectors').addBatch({ 6 | 'An custom selector with a present name': { 7 | topic: '"selector"'.match(expressions.jsString('selector')), 8 | 'should return one match': function(topic) { 9 | assert.equal(topic.length, 1); 10 | } 11 | }, 12 | 'An custom selector without a present name': { 13 | topic: '"selector"'.match(expressions.jsString('undefined')), 14 | 'should not return a match': function(topic) { 15 | assert.equal(topic, null); 16 | } 17 | } 18 | }).export(module); -------------------------------------------------------------------------------- /test/expressions/expressions.selector-name.test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'), 2 | assert = require('assert'), 3 | expressions = require('../../lib/utils/expressions'); 4 | 5 | vows.describe('Expressions: selector name').addBatch({ 6 | 'A class definition': { 7 | topic: '.selector'.match(expressions.selectorName), 8 | 'should return just the class': function(topic) { 9 | assert.equal(topic[0], 'selector'); 10 | } 11 | }, 12 | 'An id definition': { 13 | topic: '#selector'.match(expressions.selectorName), 14 | 'should return just the id': function(topic) { 15 | assert.equal(topic[0], 'selector'); 16 | } 17 | }, 18 | 'A quoted string': { 19 | topic: '"selector"'.match(expressions.selectorName), 20 | 'should return just the class': function(topic) { 21 | assert.equal(topic[0], 'selector'); 22 | } 23 | }, 24 | 'A differently quoted attribute': { 25 | topic: "'selector'".match(expressions.selectorName), 26 | 'should return just the class': function(topic) { 27 | assert.equal(topic[0], 'selector'); 28 | } 29 | }, 30 | 'Multiple classes in a string': { 31 | topic: '"selector1, selector2'.match(expressions.selectorName), 32 | 'should all be returned': function(topic) { 33 | assert.equal(topic.length, 2); 34 | } 35 | } 36 | }).export(module); -------------------------------------------------------------------------------- /test/generate-shortname.test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'), 2 | assert = require('assert'), 3 | generateShortname = require('../lib/utils/generate-shortname'); 4 | 5 | vows.describe('Generating Shortnames').addBatch({ 6 | //standard cases 7 | 'A seed of 0': { 8 | topic: generateShortname(0), 9 | 'should return the shortname "a"': function(topic) { 10 | assert.equal(topic, 'a'); 11 | } 12 | }, 13 | 'A seed of 1': { 14 | topic: generateShortname(1), 15 | 'should return the shortname "ab"': function(topic) { 16 | assert.equal(topic, 'b'); 17 | } 18 | }, 19 | 'A seed of 26': { 20 | topic: generateShortname(26), 21 | 'should return the shortname "aa"': function(topic) { 22 | assert.equal(topic, 'aa'); 23 | } 24 | }, 25 | 'A seed of 27': { 26 | topic: generateShortname(27), 27 | 'should return the shortname "ab"': function(topic) { 28 | assert.equal(topic, 'ab'); 29 | } 30 | }, 31 | //edge cases 32 | 'A seed of 1234': { 33 | topic: generateShortname(1234), 34 | 'should return the shortname "aum"': function(topic) { 35 | assert.equal(topic, 'aum'); 36 | } 37 | } 38 | }).export(module); 39 | -------------------------------------------------------------------------------- /test/library.test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'), 2 | assert = require('assert'), 3 | Library = require('../lib/utils/library'); 4 | 5 | var testLibrary = new Library(); 6 | 7 | vows.describe('Libraries').addBatch({ 8 | 'Calling the module with a new string': { 9 | topic: new Library(), 10 | 'should give us a new library to play with': function(topic) { 11 | //this one is here to make sure the API is intact 12 | assert(topic.has); 13 | assert(topic.get); 14 | assert(topic.getAll); 15 | assert(topic.getUnused); 16 | assert(topic.size); 17 | } 18 | }, 19 | 'Calling has with an undefined name': { 20 | topic: testLibrary.has('undefined'), 21 | 'should return false' : function(topic) { 22 | assert.equal(topic, false); 23 | } 24 | }, 25 | 'Calling has with a defined name': { 26 | topic: function() { 27 | testLibrary.get('defined'); 28 | return testLibrary.has('defined'); 29 | }, 30 | 'should return false' : function(topic) { 31 | assert.equal(topic, true); 32 | } 33 | }, 34 | 'Calling get twice for the same name': { 35 | topic: testLibrary.get('defined'), 36 | 'should return the same shortname' : function(topic) { 37 | assert.equal(topic, testLibrary.get('defined')); 38 | } 39 | }, 40 | 'Get all': { 41 | topic: function() { 42 | var library = new Library(); 43 | library.get('defined'); 44 | library.get('another'); 45 | return library; 46 | }, 47 | 'should return all shortnames in the library' : function(topic) { 48 | assert.deepEqual(topic.getAll(), [ 49 | topic.get('defined'), 50 | topic.get('another') 51 | ]); 52 | } 53 | }, 54 | 'Get unused': { 55 | topic: function() { 56 | var library = new Library(); 57 | library.get('defined'); 58 | library.get('defined'); 59 | library.get('unused', true); 60 | return library; 61 | }, 62 | 'should return only unused shortnames': function(topic) { 63 | assert.deepEqual(topic.getUnused(), [ 64 | topic.get('unused') 65 | ]); 66 | } 67 | }, 68 | 'Size of an empty library': { 69 | topic: function() { 70 | return new Library().size(); 71 | }, 72 | 'should be 0': function(topic) { 73 | assert.equal(topic, 0); 74 | } 75 | }, 76 | 'Size of a non empty library': { 77 | topic: function() { 78 | var library = new Library(); 79 | library.get('defined'); 80 | return library.size(); 81 | }, 82 | 'should be 1': function(topic) { 83 | assert.equal(topic, 1); 84 | } 85 | }, 86 | 'Adding ignored entries': { 87 | topic: function() { 88 | var library = new Library(['ignored']); 89 | return library.get('ignored'); 90 | }, 91 | 'should return the full name': function(topic) { 92 | assert.equal(topic, 'ignored'); 93 | } 94 | }, 95 | 'Setting dontCount to true': { 96 | topic: function() { 97 | var library = new Library(); 98 | library.get('defined', true); 99 | return library.getUnused().length; 100 | }, 101 | 'should leave the selector as unused': function(topic) { 102 | assert.deepEqual(topic, 1) 103 | } 104 | } 105 | }).export(module); -------------------------------------------------------------------------------- /test/processor-utils.test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'), 2 | assert = require('assert'), 3 | utils = require('../lib/utils/processor-utils'); 4 | 5 | var testProcessors = { 6 | css: ['html', 'js'] 7 | }, 8 | mappedProcessors = utils.extendDefaults(testProcessors); 9 | 10 | vows.describe('Processor utilities').addBatch({ 11 | 'Extending defaults names with an empty argument': { 12 | topic: utils.extendDefaults(), 13 | 'should return css and html default processors': function(topic) { 14 | assert.deepEqual(topic, { 15 | css: ['css'], 16 | html: ['html'] 17 | }); 18 | } 19 | }, 20 | 'Extending defaults names with redefined css arguments': { 21 | topic: utils.extendDefaults(testProcessors), 22 | 'should return different css values': function(topic) { 23 | assert.deepEqual(topic, { 24 | css: ['html', 'js'], 25 | html: ['html'] 26 | }); 27 | } 28 | }, 29 | 'Extending defaults names with a random argument': { 30 | topic: utils.extendDefaults({'js-strings': ['js']}), 31 | 'should return that random argument': function(topic) { 32 | assert.deepEqual(topic['js-strings'], ['js']); 33 | } 34 | }, 35 | 'Getting processors for an extension': { 36 | topic: function() { 37 | return utils.getForExtension(mappedProcessors, 'js'); 38 | }, 39 | 'should return the module for that extension': function(topic) { 40 | assert.deepEqual(topic, [ 41 | require('../lib/processors/css') 42 | ]); 43 | } 44 | }, 45 | 'Getting processors for an extension with multiple processors': { 46 | topic: function() { 47 | return utils.getForExtension(mappedProcessors, 'html'); 48 | }, 49 | 'should return all processors in the correct order': function(topic) { 50 | assert.deepEqual(topic, [ 51 | require('../lib/processors/css'), 52 | require('../lib/processors/html') 53 | ]); 54 | } 55 | } 56 | }).export(module); -------------------------------------------------------------------------------- /test/processors/css.test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'), 2 | assert = require('assert'), 3 | css = require('../../lib/processors/css'), 4 | Library = require('../../lib/utils/library'); 5 | 6 | vows.describe('CSS Replacer').addBatch({ 7 | 'A class name': { 8 | topic: '.hello-selector{property:value;}', 9 | 'should be minified': function(topic) { 10 | var classLibrary = new Library(), 11 | idLibrary = new Library(), 12 | minified = css(topic, classLibrary, idLibrary); 13 | 14 | assert.equal(minified, '.a{property:value;}') 15 | } 16 | }, 17 | 'A class name with an ignore': { 18 | topic: '.a{color: red;} .cat{color:blue}', 19 | 'should not redefine other classes with the ignored class name': function(topic) { 20 | var classLibrary = new Library(['a']), 21 | minified = css(topic, classLibrary); 22 | 23 | assert.equal(minified, '.a{color: red;} .b{color:blue}'); 24 | } 25 | }, 26 | 'A capitalized class name with an ignore': { 27 | topic: '.a{color: red;} .Cat{color:blue}', 28 | 'should not redefine other classes with the ignored class name': function(topic) { 29 | var classLibrary = new Library(['Cat']), 30 | minified = css(topic, classLibrary); 31 | 32 | assert.equal(minified, '.a{color: red;} .Cat{color:blue}'); 33 | } 34 | }, 35 | 'An id name': { 36 | topic: '#hello-selector{property:value;}', 37 | 'should be minified': function(topic) { 38 | var classLibrary = new Library(), 39 | idLibrary = new Library(), 40 | minified = css(topic, classLibrary, idLibrary); 41 | 42 | assert.equal(minified, '#a{property:value;}') 43 | } 44 | }, 45 | 'A hex value': { 46 | topic: '.a{color:#eee;}.b{color:#666}', 47 | 'should not be minified': function(topic) { 48 | var classLibrary = new Library(), 49 | idLibrary = new Library(), 50 | minified = css(topic, classLibrary, idLibrary); 51 | 52 | assert.equal(minified, '.a{color:#eee;}.b{color:#666}') 53 | } 54 | }, 55 | 'A file extension in a stylesheet': { 56 | topic: '.selector{background: url(file.extension);}', 57 | 'should not be matched': function(topic) { 58 | var classLibrary = new Library(), 59 | minified = css(topic, classLibrary); 60 | 61 | assert.equal(minified, '.a{background: url(file.extension);}'); 62 | } 63 | } 64 | }).export(module); 65 | -------------------------------------------------------------------------------- /test/processors/html.test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'), 2 | assert = require('assert'), 3 | html = require('../../lib/processors/html'), 4 | Library = require('../../lib/utils/library'); 5 | 6 | vows.describe('HTML Replacer').addBatch({ 7 | 'A class attribute': { 8 | topic: '
', 9 | 'should be minified': function(topic) { 10 | var classLibrary = new Library(), 11 | idLibrary = new Library(), 12 | minified = html(topic, classLibrary, idLibrary); 13 | 14 | assert.equal(minified, '
') 15 | } 16 | }, 17 | 'An id name': { 18 | topic: '
', 19 | 'should be minified': function(topic) { 20 | var classLibrary = new Library(), 21 | idLibrary = new Library(), 22 | minified = html(topic, classLibrary, idLibrary); 23 | 24 | assert.equal(minified, '
') 25 | } 26 | }, 27 | 'A hex value': { 28 | topic: '
', 29 | 'should not be minified': function(topic) { 30 | var classLibrary = new Library(), 31 | idLibrary = new Library(), 32 | minified = html(topic, classLibrary, idLibrary); 33 | 34 | assert.equal(minified, '
') 35 | } 36 | } 37 | }).export(module); --------------------------------------------------------------------------------