├── .travis.yml ├── test ├── fixtures │ ├── simple.css │ ├── sprite.png │ ├── no-underscores.css │ ├── no-overqualifying.css │ ├── prefixes.css │ ├── universal-selectors.css │ ├── property-order.css │ ├── no-IDs.css │ ├── inline-images.css │ ├── zero-units.css │ ├── no-JS.css │ ├── blog.css │ └── preboot.less ├── compiled │ ├── simple.css │ ├── no-underscores.css │ ├── no-overqualifying.css │ ├── prefixes.css │ ├── universal-selectors.css │ ├── no-IDs.css │ ├── property-order.css │ ├── zero-units.css │ ├── no-JS.css │ ├── blog.css │ ├── inline-images.css │ └── preboot.css ├── sprite.png ├── index.js └── types │ ├── errors.js │ ├── compile.js │ └── lint.js ├── .gitattributes ├── makefile ├── .gitignore ├── package.json ├── lib ├── compile │ ├── zero-units.js │ ├── strict-property-order.js │ ├── inline-images.js │ └── prefix-whitespace.js ├── util.js ├── lint │ ├── inline-images.js │ ├── no-IDs.js │ ├── no-JS-prefix.js │ ├── no-universal-selectors.js │ ├── no-underscores.js │ ├── no-overqualifying.js │ ├── zero-units.js │ └── strict-property-order.js ├── index.js ├── core.js └── min.js ├── benchmark └── index.js ├── bin └── recess ├── README.md └── LICENSE /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 4 | -------------------------------------------------------------------------------- /test/fixtures/simple.css: -------------------------------------------------------------------------------- 1 | body { 2 | position: absolute; 3 | display: block; 4 | } -------------------------------------------------------------------------------- /test/compiled/simple.css: -------------------------------------------------------------------------------- 1 | body { 2 | position: absolute; 3 | display: block; 4 | } -------------------------------------------------------------------------------- /test/sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twitter-archive/recess/HEAD/test/sprite.png -------------------------------------------------------------------------------- /test/fixtures/sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twitter-archive/recess/HEAD/test/fixtures/sprite.png -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('./types/lint') 3 | require('./types/compile') 4 | require('./types/errors') -------------------------------------------------------------------------------- /test/compiled/no-underscores.css: -------------------------------------------------------------------------------- 1 | .foo_bar_baz { 2 | display: block; 3 | } 4 | 5 | html, 6 | div.i_am_fat + bar, 7 | body { 8 | display: block; 9 | } -------------------------------------------------------------------------------- /test/fixtures/no-underscores.css: -------------------------------------------------------------------------------- 1 | .foo_bar_baz { 2 | display: block; 3 | } 4 | 5 | html, 6 | div.i_am_fat + bar /*a asdfasdf */, 7 | body { 8 | display: block; 9 | } -------------------------------------------------------------------------------- /test/fixtures/no-overqualifying.css: -------------------------------------------------------------------------------- 1 | div.fat { 2 | position: absolute; 3 | display: block; 4 | } 5 | 6 | h1, 7 | .foo h2#ded, 8 | h3.mdo { 9 | overflow: hidden; 10 | } -------------------------------------------------------------------------------- /test/compiled/no-overqualifying.css: -------------------------------------------------------------------------------- 1 | div.fat { 2 | position: absolute; 3 | display: block; 4 | } 5 | 6 | h1, 7 | .foo h2#ded, 8 | h3.mdo { 9 | overflow: hidden; 10 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behaviour, in case users don't have core.autocrlf set. 2 | * text=lf 3 | * text eol=lf 4 | *.* eol=lf 5 | 6 | *.png binary 7 | *.jpg binary 8 | *.gif binary -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Run all tests 3 | # 4 | test: 5 | @@ node test 6 | 7 | # 8 | # Run benchmark 9 | # 10 | benchmark: 11 | @@ node benchmark 12 | 13 | 14 | .PHONY: test benchmark -------------------------------------------------------------------------------- /test/fixtures/prefixes.css: -------------------------------------------------------------------------------- 1 | div { 2 | -ms-border-radius: 2px; 3 | -webkit-border-radius: 2px; 4 | -moz-border-radius: 2px; 5 | border-radius: 2px; 6 | -o-border-radius: 2px; 7 | } -------------------------------------------------------------------------------- /test/compiled/prefixes.css: -------------------------------------------------------------------------------- 1 | div { 2 | -webkit-border-radius: 2px; 3 | -moz-border-radius: 2px; 4 | -ms-border-radius: 2px; 5 | -o-border-radius: 2px; 6 | border-radius: 2px; 7 | } -------------------------------------------------------------------------------- /test/fixtures/universal-selectors.css: -------------------------------------------------------------------------------- 1 | * { 2 | display: block; 3 | } 4 | 5 | .ded > *, 6 | .mdo > *, 7 | .fat > * { 8 | position: absolute; 9 | } 10 | 11 | .fat * { 12 | overflow: hidden 13 | } -------------------------------------------------------------------------------- /test/compiled/universal-selectors.css: -------------------------------------------------------------------------------- 1 | * { 2 | display: block; 3 | } 4 | 5 | .ded > *, 6 | .mdo > *, 7 | .fat > * { 8 | position: absolute; 9 | } 10 | 11 | .fat * { 12 | overflow: hidden; 13 | } -------------------------------------------------------------------------------- /test/compiled/no-IDs.css: -------------------------------------------------------------------------------- 1 | div, 2 | #div:hover { 3 | position: absolute; 4 | } 5 | 6 | p, 7 | .cool, 8 | #bar { 9 | display: block; 10 | } 11 | 12 | p, 13 | .cool, 14 | .foo > #bar { 15 | display: block; 16 | } -------------------------------------------------------------------------------- /test/fixtures/property-order.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | h1, 4 | h3:hover { 5 | color: red; 6 | display: block; 7 | position: absolute; 8 | background: nutmeg; 9 | font: arial 12px/2px bold; 10 | font-size:2px; 11 | } -------------------------------------------------------------------------------- /test/compiled/property-order.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | h1, 4 | h3:hover { 5 | position: absolute; 6 | display: block; 7 | font: arial 12px/2px bold; 8 | font-size: 2px; 9 | color: red; 10 | background: nutmeg; 11 | } -------------------------------------------------------------------------------- /test/fixtures/no-IDs.css: -------------------------------------------------------------------------------- 1 | div, #div:hover { 2 | position: absolute; 3 | } 4 | 5 | p, 6 | .cool, 7 | #bar /*a sfasdasd */ { 8 | display: block; 9 | } 10 | 11 | p, 12 | .cool, 13 | .foo > #bar{ 14 | display: block; 15 | } -------------------------------------------------------------------------------- /test/compiled/zero-units.css: -------------------------------------------------------------------------------- 1 | div { 2 | position: absolute; 3 | margin: 0; 4 | margin: 0 auto; 5 | margin: 0 2px 3px; 6 | color: rgba(0, 0, 0, 0.3); 7 | color: #0ebcdc; 8 | } 9 | 10 | h1, 11 | h2 { 12 | font: Arial 0; 13 | } -------------------------------------------------------------------------------- /test/compiled/no-JS.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | .js-one, 4 | .js-two, 5 | h4, 6 | h5 { 7 | display: block; 8 | } 9 | 10 | html, 11 | body, 12 | .js-three, 13 | .js-four { 14 | display: block; 15 | } 16 | 17 | .js-five, 18 | h1, 19 | .js-six { 20 | display: block; 21 | } -------------------------------------------------------------------------------- /test/fixtures/inline-images.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | background-image: url("sprite.png"); 3 | } 4 | .bar { 5 | background: url('sprite.png'); 6 | } 7 | .fat { 8 | background: url(sprite.png); 9 | } 10 | .woo { 11 | background: #fff url(../sprite.png) center center no-repeat; 12 | } -------------------------------------------------------------------------------- /test/fixtures/zero-units.css: -------------------------------------------------------------------------------- 1 | div { 2 | position: absolute; 3 | margin: 0px; 4 | margin: 0 px; 5 | margin: 0 em; 6 | margin: 0 pt; 7 | margin: 0 pt auto; 8 | margin: 0px 2px 3px; 9 | color: rgba(0,0,0,.30); 10 | color: #0ebcdc; 11 | } 12 | 13 | h1, 14 | h2 { 15 | font: Arial 0em; 16 | } -------------------------------------------------------------------------------- /test/fixtures/no-JS.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | /* fooo 4 | * 5 | * 6 | */ 7 | .js-one, 8 | .js-two, 9 | h4, /* bar */ 10 | h5 { 11 | display: block; 12 | } 13 | 14 | html, 15 | body, 16 | .js-three, .js-four { 17 | display: block; 18 | } 19 | 20 | 21 | .js-five, h1, .js-six 22 | /* blah */ 23 | 24 | { 25 | display: block; 26 | } -------------------------------------------------------------------------------- /test/types/errors.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | , colors = require('colors') 3 | , RECESS = require('../../lib'); 4 | 5 | //error 6 | !function () { 7 | 8 | RECESS('./foo.less', function (err, instance) { 9 | assert.ok(err.length == 1) 10 | assert.ok(/Error:/.test(err[0].toString())) 11 | assert.ok(!!instance) 12 | console.log("✓ erroring".green) 13 | }) 14 | 15 | }() -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Numerous always-ignore extensions 2 | *.diff 3 | *.err 4 | *.orig 5 | *.log 6 | *.rej 7 | *.swo 8 | *.swp 9 | *.vi 10 | *~ 11 | *.sass-cache 12 | 13 | # OS or Editor folders 14 | .DS_Store 15 | ._* 16 | Thumbs.db 17 | .cache 18 | .project 19 | .settings 20 | .tmproj 21 | *.esproj 22 | nbproject 23 | *.sublime-project 24 | *.sublime-workspace 25 | 26 | # Komodo 27 | *.komodoproject 28 | .komodotools 29 | 30 | # Folders to ignore 31 | .hg 32 | .svn 33 | .CVS 34 | .idea 35 | 36 | #node modules 37 | node_modules 38 | -------------------------------------------------------------------------------- /test/types/compile.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | , assert = require('assert') 3 | , colors = require('colors') 4 | , RECESS = require('../../lib') 5 | 6 | fs.readdirSync('test/fixtures').forEach(function (file, index) { 7 | // Ignore anything not a less/css file. 8 | if (file.indexOf('css') === -1 && file.indexOf('less') === -1) { 9 | return 10 | } 11 | 12 | RECESS('test/fixtures/' + file, { compile: true, inlineImages: true }, function (err, fat) { 13 | file = file.replace(/less$/, 'css') 14 | assert.ok(err == null) 15 | assert.ok(fat[0].output[0] == fs.readFileSync('test/compiled/' + file, 'utf-8')) 16 | }) 17 | 18 | }) 19 | 20 | console.log("✓ compiling".green) 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name": "recess" 2 | , "description": "A simple, attractive code quality tool for CSS built on top of LESS" 3 | , "version": "1.1.9" 4 | , "author": "Jacob Thornton (https://github.com/fat)" 5 | , "repository": { "type": "git", "url": "git://github.com/twitter/recess.git" } 6 | , "keywords": ["css", "lint"] 7 | , "licenses": [ { "type": "Apache-2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0" } ] 8 | , "main": "./lib" 9 | , "homepage": "http://twitter.github.com/recess" 10 | , "engines": { "node": ">= 0.4.0" } 11 | , "dependencies": { "colors": ">= 0.3.0", "nopt": ">= 1.0.10", "underscore": ">= 1.2.1", "watch": ">= 0.5.1", "less": "~1.3.0" } 12 | , "directories": { "bin": "./bin" } 13 | , "scripts": { "test": "node test" } 14 | , "bin": { "recess": "./bin/recess" } 15 | , "preferGlobal": true } -------------------------------------------------------------------------------- /lib/compile/zero-units.js: -------------------------------------------------------------------------------- 1 | // ========================================== 2 | // RECESS 3 | // COMPILE: remove units from 0 values 4 | // ========================================== 5 | // Copyright 2012 Twitter, Inc 6 | // Licensed under the Apache License v2.0 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // ========================================== 9 | 10 | 'use strict' 11 | 12 | var less = require('less') 13 | , toCSS 14 | , units = [ 15 | '%' 16 | , 'in' 17 | , 'cm' 18 | , 'mm' 19 | , 'em' 20 | , 'ex' 21 | , 'pt' 22 | , 'pc' 23 | , 'px' 24 | ] 25 | , UNITS = new RegExp('\\b0\\s?(' + units.join('|') + ')') 26 | 27 | function compile () { 28 | // strip units from 0 values 29 | var props = toCSS.apply(this, arguments) 30 | 31 | // don't strip chars from hex codes 32 | return /#/.test(props) ? props : props.replace(UNITS, '0') 33 | } 34 | 35 | module.exports.on = function () { 36 | toCSS = less.tree.Value.prototype.toCSS 37 | less.tree.Value.prototype.toCSS = compile 38 | } 39 | 40 | module.exports.off = function () { 41 | less.tree.Value.prototype.toCSS = toCSS 42 | } -------------------------------------------------------------------------------- /lib/compile/strict-property-order.js: -------------------------------------------------------------------------------- 1 | // ========================================== 2 | // RECESS 3 | // COMPILE: automatically sort properties 4 | // ========================================== 5 | // Copyright 2012 Twitter, Inc 6 | // Licensed under the Apache License v2.0 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // ========================================== 9 | 10 | 'use strict' 11 | 12 | var less = require('less') 13 | , order = require('../lint/strict-property-order') 14 | , toCSS 15 | 16 | 17 | function compile (context, env) { 18 | var l 19 | 20 | // test property order 21 | order(this, env.data) 22 | 23 | // search errors for sortedRules property 24 | if (this.errors) { 25 | for (l = this.errors.length; l--;) { 26 | 27 | // if sorted rule found apply it, then exit 28 | if (this.errors[l].sortedRules) { 29 | this.rules = this.errors[l].sortedRules 30 | break 31 | } 32 | 33 | } 34 | } 35 | 36 | // apply old toCSS method to updated object 37 | return toCSS.apply(this, arguments) 38 | } 39 | 40 | module.exports.on = function () { 41 | toCSS = less.tree.Ruleset.prototype.toCSS 42 | less.tree.Ruleset.prototype.toCSS = compile 43 | } 44 | 45 | module.exports.off = function () { 46 | less.tree.Ruleset.prototype.toCSS = toCSS 47 | } -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | // ========================================== 2 | // RECESS 3 | // UTIL: simple output util methods 4 | // ========================================== 5 | // Copyright 2012 Twitter, Inc 6 | // Licensed under the Apache License v2.0 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // ========================================== 9 | 10 | 'use strict' 11 | 12 | var _ = require('underscore') 13 | 14 | module.exports = { 15 | 16 | // set fail output object 17 | throwError: function (def, err) { 18 | def.errors = def.errors || [] 19 | err.message = err.message.cyan 20 | def.errors.push(err) 21 | } 22 | 23 | // set line padding 24 | , padLine: function (line) { 25 | var num = (line + '. ') 26 | , space = '' 27 | _.times(10 - num.length, function () { space += ' ' }) 28 | return (space + num).grey 29 | } 30 | 31 | // get line number from data 32 | , getLine: function (index, data) { 33 | return (data.slice(0, index).match(/\n/g) || "").length + 1; 34 | } 35 | 36 | // error counter 37 | , countErrors: function (definitions) { 38 | var fails = 0 39 | definitions.forEach(function (def) { 40 | def.errors 41 | && def.errors.length 42 | && def.errors.forEach(function (err) { fails++ }) 43 | }) 44 | return fails 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /benchmark/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var path = require('path') 3 | , fs = require('fs') 4 | , less = require('less') 5 | , recess = require('../lib') 6 | , file = path.join(__dirname, 'benchmark.less') 7 | 8 | fs.readFile(file, 'utf8', function (e, data) { 9 | var css, start, end 10 | 11 | new(less.Parser)({ optimization: 2 }).parse(data, function (err, tree) { 12 | 13 | start = new Date() 14 | css = tree.toCSS() 15 | end = new Date() 16 | 17 | console.log( 18 | " LESS toCSS: " 19 | + (end - start) 20 | + " ms (" 21 | + parseInt(1000 / (end - start) * data.length / 1024) 22 | + " KB\/s)" 23 | ) 24 | 25 | new(less.Parser)({ optimization: 2 }).parse(css, function (err, tree) { 26 | 27 | var play = new recess.Constructor(false) 28 | 29 | play.data = css 30 | play.definitions = tree.rules 31 | play.path = 'less' 32 | play.callback = function () { 33 | end = new Date() 34 | console.log( 35 | "RECESS toCSS: " 36 | + (end - start) 37 | + " ms (" 38 | + parseInt(1000 / (end - start) * data.length / 1024) 39 | + " KB\/s)" 40 | ) 41 | } 42 | start = new Date() 43 | play.compile() 44 | }) 45 | 46 | }) 47 | }) -------------------------------------------------------------------------------- /lib/compile/inline-images.js: -------------------------------------------------------------------------------- 1 | // ========================================== 2 | // RECESS 3 | // COMPILE: replaces image links with base64 image data 4 | // ========================================== 5 | // Copyright 2012 Twitter, Inc 6 | // Licensed under the Apache License v2.0 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // ========================================== 9 | 10 | 'use strict' 11 | 12 | var less = require('less') 13 | , fs = require('fs') 14 | , seperator = (process.platform == 'win32') ? '\\' : '/' 15 | , toCSS 16 | , path 17 | 18 | function compile () { 19 | // strip units from 0 values 20 | var props = toCSS.apply(this, arguments) 21 | 22 | // do we have a url here? 23 | if (/url\(/.test(props)) { 24 | var fileName = props.match(/url\((['"]?)(.*)\1\)/)[2] 25 | , ext = fileName.match(/[^.]*$/)[0] 26 | , mimetype = 'image/' + ext.replace(/jpg/, 'jpeg') 27 | , pathParts = path.split(seperator) 28 | , filePath = pathParts.slice(0, pathParts.length - 1).join(seperator) 29 | , imgBuffer = new Buffer(fs.readFileSync((filePath?filePath:'.')+seperator+fileName)).toString('base64') 30 | , urlData = 'url(data:' + mimetype + ';base64,' + imgBuffer + ')' 31 | 32 | return props.replace(/url\([^\)]*\)/, urlData) 33 | } 34 | 35 | return props 36 | } 37 | 38 | module.exports.on = function () { 39 | path = this.path 40 | toCSS = less.tree.Value.prototype.toCSS 41 | less.tree.Value.prototype.toCSS = compile 42 | } 43 | 44 | module.exports.off = function () { 45 | less.tree.Value.prototype.toCSS = toCSS 46 | } -------------------------------------------------------------------------------- /lib/lint/inline-images.js: -------------------------------------------------------------------------------- 1 | // =================================================== 2 | // RECESS 3 | // RULE: Linked images should be embeded. 4 | // =================================================== 5 | // Copyright 2012 Twitter, Inc 6 | // Licensed under the Apache License v2.0 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // =================================================== 9 | 10 | 'use strict' 11 | 12 | var util = require('../util') 13 | , RULE = { 14 | type: 'inlineImages' 15 | , exp: /^url\((?!data:)/ 16 | , message: 'Linked images should be embeded.' 17 | } 18 | 19 | // validation method 20 | module.exports = function (def, data) { 21 | 22 | // default validation to true 23 | var isValid = true 24 | 25 | // return if no selector to validate 26 | if (!def.rules) return isValid 27 | 28 | // loop over selectors 29 | def.rules.forEach(function (rule) { 30 | var extract 31 | , line 32 | 33 | // continue to next rule if no url is present 34 | if ( !(rule.value 35 | && rule.value.is == 'value' 36 | && RULE.exp.test(rule.value.toCSS({}))) ) return 37 | 38 | // calculate line number for the extract 39 | line = util.getLine(rule.index, data) 40 | extract = util.padLine(line) 41 | 42 | // highlight invalid 0 units 43 | extract += rule.toCSS({}).replace(RULE.exp, function ($1) { 44 | return $1.magenta 45 | }) 46 | 47 | // set invalid flag to false 48 | isValid = false 49 | 50 | // set error object on defintion token 51 | util.throwError(def, { 52 | type: RULE.type 53 | , message: RULE.message 54 | , extract: extract 55 | , line: line 56 | }) 57 | 58 | }) 59 | 60 | // return validation state 61 | return isValid 62 | } -------------------------------------------------------------------------------- /lib/lint/no-IDs.js: -------------------------------------------------------------------------------- 1 | // ========================================== 2 | // RECESS 3 | // RULE: Id's should not be styled 4 | // ========================================== 5 | // Copyright 2012 Twitter, Inc 6 | // Licensed under the Apache License v2.0 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // ========================================== 9 | 10 | 'use strict' 11 | 12 | var util = require('../util') 13 | , RULE = { 14 | type: 'noIDs' 15 | , exp: /^#/ 16 | , message: 'Id\'s should not be styled' 17 | } 18 | 19 | // validation method 20 | module.exports = function (def, data) { 21 | 22 | // default validation to true 23 | var isValid = true 24 | 25 | // return if no selectors to validate 26 | if (!def.selectors) return isValid 27 | 28 | // loop over selectors 29 | def.selectors.forEach(function (selector) { 30 | 31 | // loop over selector entities 32 | selector.elements.forEach(function (element) { 33 | 34 | var extract 35 | , line 36 | 37 | // continue to next element if no js- prefix 38 | if (!RULE.exp.test(element.value)) return 39 | 40 | // calculate line number for the extract 41 | line = util.getLine(element.index - element.value.length, data) 42 | extract = util.padLine(line) 43 | 44 | // highlight invalid styling of ID 45 | extract += element.value.replace(RULE.exp, '#'.magenta) 46 | 47 | // set invalid flag to false 48 | isValid = false 49 | 50 | // set error object on defintion token 51 | util.throwError(def, { 52 | type: RULE.type 53 | , message: RULE.message 54 | , extract: extract 55 | , line: line 56 | }) 57 | 58 | }) 59 | }) 60 | 61 | // return valid state 62 | return isValid 63 | } -------------------------------------------------------------------------------- /lib/lint/no-JS-prefix.js: -------------------------------------------------------------------------------- 1 | // ========================================== 2 | // RECESS 3 | // RULE: .js prefixes should not be styled 4 | // ========================================== 5 | // Copyright 2012 Twitter, Inc 6 | // Licensed under the Apache License v2.0 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // ========================================== 9 | 10 | 'use strict' 11 | 12 | var util = require('../util') 13 | , RULE = { 14 | type: 'noJSPrefix' 15 | , exp: /^\.js\-/ 16 | , message: '.js prefixes should not be styled' 17 | } 18 | 19 | // validation method 20 | module.exports = function (def, data) { 21 | 22 | // default validation to true 23 | var isValid = true 24 | 25 | // return if no selector to validate 26 | if (!def.selectors) return isValid 27 | 28 | // loop over selectors 29 | def.selectors.forEach(function (selector) { 30 | 31 | // loop over selector entities 32 | selector.elements.forEach(function (element) { 33 | 34 | var extract 35 | , line 36 | 37 | // continue to next element if .js- prefix not styled 38 | if (!RULE.exp.test(element.value)) return 39 | 40 | // calculate line number for the extract 41 | line = util.getLine(element.index - element.value.length, data) 42 | extract = util.padLine(line) 43 | 44 | // highlight invalid styling of .js- prefix 45 | extract += element.value.replace(RULE.exp, '.js-'.magenta) 46 | 47 | // set invalid flag to false 48 | isValid = false 49 | 50 | // set error object on defintion token 51 | util.throwError(def, { 52 | type: RULE.type 53 | , message: RULE.message 54 | , extract: extract 55 | , line: line 56 | }) 57 | 58 | }) 59 | 60 | }) 61 | 62 | // return valid state 63 | return isValid 64 | } -------------------------------------------------------------------------------- /lib/lint/no-universal-selectors.js: -------------------------------------------------------------------------------- 1 | // =========================================== 2 | // RECESS 3 | // RULE: Universal selectors should be avoided 4 | // =========================================== 5 | // Copyright 2012 Twitter, Inc 6 | // Licensed under the Apache License v2.0 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // =========================================== 9 | 10 | 'use strict' 11 | 12 | var util = require('../util') 13 | , RULE = { 14 | type: 'noUniversalSelectors' 15 | , exp: /\*/g 16 | , message: 'Universal selectors should be avoided' 17 | } 18 | 19 | // validation method 20 | module.exports = function (def, data) { 21 | 22 | // default validation to true 23 | var isValid = true 24 | 25 | // return if no rules to validate 26 | if (!def.selectors) return isValid 27 | 28 | // loop over selectors 29 | def.selectors.forEach(function (selector) { 30 | 31 | // loop over selector entities 32 | selector.elements.forEach(function (element) { 33 | 34 | var extract 35 | , line 36 | 37 | // continue to next element if no underscore 38 | if (!RULE.exp.test(element.value)) return 39 | 40 | // calculate line number for the extract 41 | line = util.getLine(element.index - element.value.length, data) 42 | extract = util.padLine(line) 43 | 44 | // highlight the invalid use of a universal selector 45 | extract += selector.toCSS({}).replace(RULE.exp, '*'.magenta) 46 | 47 | // set invalid flag to false 48 | isValid = false 49 | 50 | // set error object on defintion token 51 | util.throwError(def, { 52 | type: RULE.type 53 | , message: RULE.message 54 | , extract: extract 55 | , line: line 56 | }) 57 | 58 | }) 59 | }) 60 | 61 | // return valid state 62 | return isValid 63 | 64 | } -------------------------------------------------------------------------------- /lib/lint/no-underscores.js: -------------------------------------------------------------------------------- 1 | // ========================================================== 2 | // RECESS 3 | // RULE: Underscores should not be used when naming selectors 4 | // ========================================================== 5 | // Copyright 2012 Twitter, Inc 6 | // Licensed under the Apache License v2.0 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // ========================================================== 9 | 10 | 'use strict' 11 | 12 | var util = require('../util') 13 | , RULE = { 14 | type: 'noUnderscores' 15 | , exp: /_/g 16 | , message: 'Underscores should not be used when naming selectors' 17 | } 18 | 19 | // validation method 20 | module.exports = function validate(def, data) { 21 | 22 | // default validation to true 23 | var isValid = true 24 | 25 | // return if no selector to validate 26 | if (!def.selectors) return isValid 27 | 28 | // loop over selectors 29 | def.selectors.forEach(function (selector) { 30 | 31 | // loop over selector entities 32 | selector.elements.forEach(function (element) { 33 | 34 | var extract 35 | , line 36 | 37 | // continue to next element if no underscore 38 | if (!RULE.exp.test(element.value)) return 39 | 40 | // calculate line number for the extract 41 | line = util.getLine(element.index - element.value.length, data) 42 | extract = util.padLine(line) 43 | 44 | // highlight invalid underscores 45 | extract += element.value.replace(RULE.exp, '_'.magenta) 46 | 47 | // set invalid flag to false 48 | isValid = false 49 | 50 | // set error object on defintion token 51 | util.throwError(def, { 52 | type: RULE.type 53 | , message: RULE.message 54 | , extract: extract 55 | , line: line 56 | }) 57 | 58 | }) 59 | }) 60 | 61 | // return valid state 62 | return isValid 63 | 64 | } -------------------------------------------------------------------------------- /lib/lint/no-overqualifying.js: -------------------------------------------------------------------------------- 1 | // =================================================== 2 | // RECESS 3 | // RULE: Element selectors should not be overqualified 4 | // =================================================== 5 | // Copyright 2012 Twitter, Inc 6 | // Licensed under the Apache License v2.0 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // =================================================== 9 | 10 | 'use strict' 11 | 12 | var util = require('../util') 13 | , RULE = { 14 | type: 'noOverqualifying' 15 | , exp: /\b[\w\-\_]+(?=#|\.)/ 16 | , message: 'Element selectors should not be overqualified' 17 | } 18 | 19 | // validation method 20 | module.exports = function (def, data) { 21 | 22 | // default validation to true 23 | var isValid = true 24 | 25 | // return if no selector to validate 26 | if (!def.selectors) return isValid 27 | 28 | // loop over selectors 29 | def.selectors.forEach(function (selector) { 30 | 31 | // evaluate selector to string and trim whitespace 32 | var selectorString = selector.toCSS().trim() 33 | , extract 34 | , line 35 | 36 | // if selector isn't overqualified continue 37 | if (!RULE.exp.test(selectorString)) return 38 | 39 | // calculate line number for the extract 40 | line = util.getLine(selector.elements[0].index - selector.elements[0].value.length, data) 41 | extract = util.padLine(line) 42 | 43 | // highlight selector overqualification 44 | extract += selectorString.replace(RULE.exp, function ($1) { return $1.magenta }) 45 | 46 | // set invalid flag to false 47 | isValid = false 48 | 49 | // set error object on defintion token 50 | util.throwError(def, { 51 | type: RULE.type 52 | , message: RULE.message 53 | , extract: extract 54 | , line: line 55 | }) 56 | 57 | }) 58 | 59 | // return validation state 60 | return isValid 61 | } -------------------------------------------------------------------------------- /lib/lint/zero-units.js: -------------------------------------------------------------------------------- 1 | // ================================================ 2 | // RECESS 3 | // RULE: No need to specify units when a value is 0 4 | // ================================================ 5 | // Copyright 2012 Twitter, Inc 6 | // Licensed under the Apache License v2.0 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // ================================================ 9 | 10 | 'use strict' 11 | 12 | var util = require('../util') 13 | , units = [ 14 | '%' 15 | , 'in' 16 | , 'cm' 17 | , 'mm' 18 | , 'em' 19 | , 'ex' 20 | , 'pt' 21 | , 'pc' 22 | , 'px' 23 | ] 24 | , RULE = { 25 | type: 'zeroUnits' 26 | , exp: new RegExp('\\b0\\s?(' + units.join('|') + ')') 27 | , message: 'No need to specify units when a value is 0' 28 | } 29 | 30 | // validation method 31 | module.exports = function (def, data) { 32 | 33 | // default validation to true 34 | var isValid = true 35 | 36 | // return if no rules to validate 37 | if (!def.rules) return isValid 38 | 39 | // loop over rules 40 | def.rules.forEach(function (rule) { 41 | var extract 42 | , line 43 | 44 | // continue to next rule if no 0 units are present 45 | if ( !(rule.value 46 | && rule.value.is == 'value' 47 | && RULE.exp.test(rule.value.toCSS({}))) ) return 48 | 49 | // calculate line number for the extract 50 | line = util.getLine(rule.index, data) 51 | extract = util.padLine(line) 52 | 53 | // highlight invalid 0 units 54 | extract += rule.toCSS({}).replace(RULE.exp, function ($1) { 55 | return 0 + $1.slice(1).magenta 56 | }) 57 | 58 | // set invalid flag to false 59 | isValid = false 60 | 61 | // set error object on defintion token 62 | util.throwError(def, { 63 | type: RULE.type 64 | , message: RULE.message 65 | , extract: extract 66 | , line: line 67 | }) 68 | 69 | }) 70 | 71 | // return valid state 72 | return isValid 73 | } -------------------------------------------------------------------------------- /test/fixtures/blog.css: -------------------------------------------------------------------------------- 1 | /* Fat's blog styles */ 2 | 3 | html, 4 | body { 5 | overflow: auto; 6 | } 7 | 8 | h1 a { 9 | margin: 0px; 10 | font-family: "Mistral", helvetica; 11 | font-size: 48px; 12 | color: #f600ff; 13 | } 14 | 15 | h1 a:hover { 16 | color: #f600ff; 17 | text-decoration: none; 18 | } 19 | 20 | body > article, 21 | body > header, 22 | body > section, 23 | body > footer { 24 | width: 525px; 25 | padding: 0 50px; 26 | } 27 | 28 | header h1 { 29 | margin-bottom: 0; 30 | } 31 | 32 | body > header { 33 | padding-top: 50px; 34 | } 35 | 36 | body > footer { 37 | padding-bottom: 20px; 38 | } 39 | 40 | body > footer, 41 | body > section { 42 | overflow: hidden; 43 | } 44 | 45 | article header { 46 | margin-bottom: 15px; 47 | } 48 | 49 | footer { 50 | margin-top: 0; 51 | border: 0; 52 | } 53 | 54 | a, 55 | a:hover { 56 | color: #000; 57 | } 58 | 59 | p a { 60 | text-decoration: underline; 61 | } 62 | 63 | p { 64 | color: #555; 65 | } 66 | 67 | .post { 68 | margin: 30px 0; 69 | } 70 | 71 | section .post img { 72 | float: left; 73 | width: 250px; 74 | margin-right: 25px; 75 | margin-bottom:0; 76 | } 77 | 78 | .post img { 79 | width: 500px; 80 | margin-bottom:-25px; 81 | } 82 | 83 | .post::after { 84 | display: block; 85 | width: auto; 86 | height: 8px; 87 | margin: 0 auto; 88 | margin-top: 31px; 89 | background-color: #000; 90 | content: ''; 91 | } 92 | 93 | .avatar { 94 | position: relative; 95 | float:left; 96 | margin-right: 25px; 97 | } 98 | 99 | .avatar img { 100 | position: relative; 101 | display: block; 102 | width: 189px; 103 | margin: 0 auto; 104 | border-radius: 10px; 105 | box-shadow: 0 1px 2px rgba(0,0,0,.3); 106 | } 107 | 108 | .avatar img::after { 109 | position: absolute; 110 | top: 0; 111 | left: 0; 112 | display: block; 113 | width: 32px; 114 | height: 32px; 115 | -moz-border-radius: 4px; 116 | -webkit-border-radius: 4px; 117 | border-radius: 4px; 118 | content: " "; 119 | -moz-box-shadow: inset 0 1px 5px rgba(0,0,0,0.2); 120 | -webkit-box-shadow: inset 0 1px 5px rgba(0,0,0,0.2); 121 | -o-box-shadow: inset 0 1px 5px rgba(0,0,0,0.2); 122 | -ms-box-shadow: inset 0 1px 5px rgba(0,0,0,0.2); 123 | box-shadow: inset 0 1px 5px rgba(0,0,0,0.2); 124 | } 125 | 126 | article p { 127 | margin-bottom: 20px; 128 | font-size: 14px; 129 | line-height: 25px; 130 | } 131 | 132 | pre code { 133 | background: transparent; 134 | } 135 | -------------------------------------------------------------------------------- /test/compiled/blog.css: -------------------------------------------------------------------------------- 1 | /* Fat's blog styles */ 2 | 3 | html, 4 | body { 5 | overflow: auto; 6 | } 7 | 8 | h1 a { 9 | margin: 0; 10 | font-family: "Mistral", helvetica; 11 | font-size: 48px; 12 | color: #f600ff; 13 | } 14 | 15 | h1 a:hover { 16 | color: #f600ff; 17 | text-decoration: none; 18 | } 19 | 20 | body > article, 21 | body > header, 22 | body > section, 23 | body > footer { 24 | width: 525px; 25 | padding: 0 50px; 26 | } 27 | 28 | header h1 { 29 | margin-bottom: 0; 30 | } 31 | 32 | body > header { 33 | padding-top: 50px; 34 | } 35 | 36 | body > footer { 37 | padding-bottom: 20px; 38 | } 39 | 40 | body > footer, 41 | body > section { 42 | overflow: hidden; 43 | } 44 | 45 | article header { 46 | margin-bottom: 15px; 47 | } 48 | 49 | footer { 50 | margin-top: 0; 51 | border: 0; 52 | } 53 | 54 | a, 55 | a:hover { 56 | color: #000; 57 | } 58 | 59 | p a { 60 | text-decoration: underline; 61 | } 62 | 63 | p { 64 | color: #555; 65 | } 66 | 67 | .post { 68 | margin: 30px 0; 69 | } 70 | 71 | section .post img { 72 | float: left; 73 | width: 250px; 74 | margin-right: 25px; 75 | margin-bottom: 0; 76 | } 77 | 78 | .post img { 79 | width: 500px; 80 | margin-bottom: -25px; 81 | } 82 | 83 | .post::after { 84 | display: block; 85 | width: auto; 86 | height: 8px; 87 | margin: 0 auto; 88 | margin-top: 31px; 89 | background-color: #000; 90 | content: ''; 91 | } 92 | 93 | .avatar { 94 | position: relative; 95 | float: left; 96 | margin-right: 25px; 97 | } 98 | 99 | .avatar img { 100 | position: relative; 101 | display: block; 102 | width: 189px; 103 | margin: 0 auto; 104 | border-radius: 10px; 105 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); 106 | } 107 | 108 | .avatar img::after { 109 | position: absolute; 110 | top: 0; 111 | left: 0; 112 | display: block; 113 | width: 32px; 114 | height: 32px; 115 | -webkit-border-radius: 4px; 116 | -moz-border-radius: 4px; 117 | border-radius: 4px; 118 | content: " "; 119 | -webkit-box-shadow: inset 0 1px 5px rgba(0, 0, 0, 0.2); 120 | -moz-box-shadow: inset 0 1px 5px rgba(0, 0, 0, 0.2); 121 | -ms-box-shadow: inset 0 1px 5px rgba(0, 0, 0, 0.2); 122 | -o-box-shadow: inset 0 1px 5px rgba(0, 0, 0, 0.2); 123 | box-shadow: inset 0 1px 5px rgba(0, 0, 0, 0.2); 124 | } 125 | 126 | article p { 127 | margin-bottom: 20px; 128 | font-size: 14px; 129 | line-height: 25px; 130 | } 131 | 132 | pre code { 133 | background: transparent; 134 | } -------------------------------------------------------------------------------- /lib/compile/prefix-whitespace.js: -------------------------------------------------------------------------------- 1 | // ========================================== 2 | // RECESS 3 | // COMPILE: whitespace for vendor prefixes 4 | // ========================================== 5 | // Copyright 2012 Twitter, Inc 6 | // Licensed under the Apache License v2.0 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // ========================================== 9 | 10 | 'use strict' 11 | 12 | var less = require('less') 13 | , toCSS 14 | 15 | // vendor prfixes 16 | , vendorPrefixes = [ 17 | '-webkit-' 18 | , '-khtml-' 19 | , '-epub-' 20 | , '-moz-' 21 | , '-ms-' 22 | , '-o-' 23 | ] 24 | , VENDOR_PREFIX = new RegExp('^(\\s*(?:' + vendorPrefixes.join('|').replace(/[-[\]{}()*+?.,\\^$#\s]/g, "\\$&") + '))') 25 | 26 | 27 | // space defintion 28 | function space(rules, i) { 29 | var rule = rules[i] 30 | , j = i - 1 31 | , peek = rules[j] 32 | , result = '' 33 | , ruleRoot 34 | , peekRoot 35 | , ruleVal 36 | , peekVal 37 | 38 | // skip if not peak, rule, or rule.name 39 | if (!peek || !rule || !rule.name) return 40 | 41 | // if previous rule is not a css property, try searching up tree for nearest rule 42 | while (!peek.name) { 43 | peek = rules[j--] 44 | 45 | // if none, then exit 46 | if (!peek) return 47 | } 48 | 49 | // check to see if name has a vnedor prefix 50 | if (VENDOR_PREFIX.test(peek.name)) { 51 | 52 | // strip vendor prefix from rule and prior rule 53 | ruleRoot = rule.name.replace(VENDOR_PREFIX, '') 54 | peekRoot = peek.name.replace(VENDOR_PREFIX, '') 55 | 56 | 57 | // if they share the same root calculate the offset in spacing 58 | if (ruleRoot === peekRoot) { 59 | 60 | // calculate the rules val 61 | ruleVal = rule.name.match(VENDOR_PREFIX) 62 | ruleVal = (ruleVal && ruleVal[0].length) || 0 63 | 64 | // calculate the peeks val 65 | peekVal = peek.name.match(VENDOR_PREFIX) 66 | peekVal = (peekVal && peekVal[0].length) || 0 67 | 68 | // if peek has a value, offset the rule val 69 | if (peekVal) { 70 | ruleVal = peekVal - ruleVal 71 | while (ruleVal--) result += ' ' 72 | } 73 | 74 | } 75 | } 76 | 77 | // prefix the rule with the white space offset 78 | rule.name = result + rule.name 79 | } 80 | 81 | function compile (context, env) { 82 | // iterate over rules and space each property 83 | for (var i = 0; i < this.rules.length; i++) { 84 | space(this.rules, i) 85 | } 86 | 87 | // apply to base CSS 88 | return toCSS.apply(this, arguments) 89 | } 90 | 91 | module.exports.on = function () { 92 | toCSS = less.tree.Ruleset.prototype.toCSS 93 | less.tree.Ruleset.prototype.toCSS = compile 94 | } 95 | 96 | module.exports.off = function () { 97 | less.tree.Ruleset.prototype.toCSS = toCSS 98 | } -------------------------------------------------------------------------------- /bin/recess: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var recess = require('../lib') 3 | , watch = require('watch') 4 | , nopt = require('nopt') 5 | , path = require('path') 6 | , fs = require('fs') 7 | , config = '.recessrc' 8 | , writeFile 9 | , options 10 | , output 11 | , paths 12 | 13 | // exit with docs 14 | if (process.argv.length == 2) return recess.docs() 15 | 16 | // define expected options 17 | options = { 18 | compile: Boolean 19 | , compress: Boolean 20 | , config: path 21 | , format: String 22 | , includePath: [path, Array] 23 | , noIDs: Boolean 24 | , noJSPrefix: Boolean 25 | , noOverqualifying: Boolean 26 | , noSummary: Boolean 27 | , noUnderscores: Boolean 28 | , noUniversalSelectors: Boolean 29 | , prefixWhitespace: Boolean 30 | , strictPropertyOrder: Boolean 31 | , version: Boolean 32 | , watch: path 33 | , zeroUnits: Boolean 34 | } 35 | 36 | // parse options 37 | shorthand = { 'v': ['--version'] }; 38 | options = nopt(options, shorthand, process.argv) 39 | 40 | // if help exit 41 | if (options.help) return recess.docs() 42 | 43 | // set version 44 | recess.version = require(path.join(__dirname, '..', 'package.json')).version 45 | if (options.version) return console.log(recess.version) 46 | 47 | // set path from remaining arguments 48 | paths = options.argv.remain 49 | 50 | // clean options object 51 | delete options.argv 52 | 53 | // check for config or default .recessrc 54 | if (options.config || (fs.existsSync || path.existsSync)(config)) { 55 | config = JSON.parse(fs.readFileSync(options.config || config)) 56 | for (i in options) config[i] = options[i] 57 | options = config 58 | } 59 | 60 | // set CLI to true 61 | options.cli = true 62 | 63 | 64 | // if not watch - run Recess 65 | if (!options.watch) return recess(paths, options) 66 | 67 | // if options watch, but compile isn't set - make it happen 68 | if (options.watch && !options.compile && !options.compress) options.compile = true; 69 | 70 | // set CLI to false 71 | options.cli = false 72 | paths = paths[0].split(':') 73 | output = paths[1] 74 | paths = paths[0] 75 | 76 | // generate output and write to file 77 | writeFile = function () { 78 | recess(paths, options, function (err, obj) { 79 | fs.writeFile(output, obj.output) 80 | }) 81 | } 82 | 83 | // if watch doesn't exist, watch the path 84 | if (!(fs.existsSync || path.existsSync)(options.watch)) options.watch = path.resolve(paths) 85 | 86 | // throw if can't find file to watch 87 | if (!(fs.existsSync || path.existsSync)(options.watch)) return console.log("can't find file: " + options.watch) 88 | 89 | // use fs.watch if watchign single file 90 | if (path.extname(options.watch)) return fs.watch(options.watch, writeFile) 91 | 92 | // create monitor to watch filetree if provided dir 93 | watch.createMonitor(options.watch, function (monitor) { 94 | monitor.on("created", writeFile) 95 | monitor.on("changed", writeFile) 96 | monitor.on("removed", writeFile) 97 | }) 98 | -------------------------------------------------------------------------------- /test/compiled/inline-images.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAcCAIAAADqTdgKAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAbJJREFUeNrslFtLAlEQx4/X42U1dUU3NaULPkRSVARBEAT17LfsO/QW9CoURQ9aBlvKJpmKl3VtL/1DUVdPKfjQi8Oy7M6c+c2cOTPHkr0sk8XEShaWJWIk9nkWNVt16bPUbtW8XHA9vmW3UWig93GBEQKqZ/Fhb/tk2v/28abaqnXdfsPDWRofolTkuSA0pwcXpo0gyBt1Y7WqKRPxoe+kdvRI0uBCeMt8DBpfev+l9GRCUH8QK2C7zl2J9VGzAQ39OBS/fc1hOmOqRczH3yll2Dpc6P61UMjnksIGuL9V51xYa3R1E0KW5Sh1vis9fCPbDiH5iuiSikibUM80Yug/QtTUTqUqkbGc+yCmIBijL9Lh+MSe/xCBWBiIrmpkY6k5ESgcuzuV3tc8/tgFdTrYCBh2KZ2JOOZXNc3KRsCQERJnUWGiWuMC64wxq7blar3WP1qmP2/3TKQwONT+FKH5f+rq9pNIcnr/yN9hsaHq7EkNrPgT4RBGsNxEbxjDLOCJ8+MDwU3OV+9qTP8BArk1NJ2jrow3cWQ35SmruqIaUkud675AEDwNoi+v339FfAswADLzuoci9jFaAAAAAElFTkSuQmCC); 3 | } 4 | 5 | .bar { 6 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAcCAIAAADqTdgKAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAbJJREFUeNrslFtLAlEQx4/X42U1dUU3NaULPkRSVARBEAT17LfsO/QW9CoURQ9aBlvKJpmKl3VtL/1DUVdPKfjQi8Oy7M6c+c2cOTPHkr0sk8XEShaWJWIk9nkWNVt16bPUbtW8XHA9vmW3UWig93GBEQKqZ/Fhb/tk2v/28abaqnXdfsPDWRofolTkuSA0pwcXpo0gyBt1Y7WqKRPxoe+kdvRI0uBCeMt8DBpfev+l9GRCUH8QK2C7zl2J9VGzAQ39OBS/fc1hOmOqRczH3yll2Dpc6P61UMjnksIGuL9V51xYa3R1E0KW5Sh1vis9fCPbDiH5iuiSikibUM80Yug/QtTUTqUqkbGc+yCmIBijL9Lh+MSe/xCBWBiIrmpkY6k5ESgcuzuV3tc8/tgFdTrYCBh2KZ2JOOZXNc3KRsCQERJnUWGiWuMC64wxq7blar3WP1qmP2/3TKQwONT+FKH5f+rq9pNIcnr/yN9hsaHq7EkNrPgT4RBGsNxEbxjDLOCJ8+MDwU3OV+9qTP8BArk1NJ2jrow3cWQ35SmruqIaUkud675AEDwNoi+v339FfAswADLzuoci9jFaAAAAAElFTkSuQmCC); 7 | } 8 | 9 | .fat { 10 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAcCAIAAADqTdgKAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAbJJREFUeNrslFtLAlEQx4/X42U1dUU3NaULPkRSVARBEAT17LfsO/QW9CoURQ9aBlvKJpmKl3VtL/1DUVdPKfjQi8Oy7M6c+c2cOTPHkr0sk8XEShaWJWIk9nkWNVt16bPUbtW8XHA9vmW3UWig93GBEQKqZ/Fhb/tk2v/28abaqnXdfsPDWRofolTkuSA0pwcXpo0gyBt1Y7WqKRPxoe+kdvRI0uBCeMt8DBpfev+l9GRCUH8QK2C7zl2J9VGzAQ39OBS/fc1hOmOqRczH3yll2Dpc6P61UMjnksIGuL9V51xYa3R1E0KW5Sh1vis9fCPbDiH5iuiSikibUM80Yug/QtTUTqUqkbGc+yCmIBijL9Lh+MSe/xCBWBiIrmpkY6k5ESgcuzuV3tc8/tgFdTrYCBh2KZ2JOOZXNc3KRsCQERJnUWGiWuMC64wxq7blar3WP1qmP2/3TKQwONT+FKH5f+rq9pNIcnr/yN9hsaHq7EkNrPgT4RBGsNxEbxjDLOCJ8+MDwU3OV+9qTP8BArk1NJ2jrow3cWQ35SmruqIaUkud675AEDwNoi+v339FfAswADLzuoci9jFaAAAAAElFTkSuQmCC); 11 | } 12 | 13 | .woo { 14 | background: #ffffff url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAcCAIAAADqTdgKAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAbJJREFUeNrslFtLAlEQx4/X42U1dUU3NaULPkRSVARBEAT17LfsO/QW9CoURQ9aBlvKJpmKl3VtL/1DUVdPKfjQi8Oy7M6c+c2cOTPHkr0sk8XEShaWJWIk9nkWNVt16bPUbtW8XHA9vmW3UWig93GBEQKqZ/Fhb/tk2v/28abaqnXdfsPDWRofolTkuSA0pwcXpo0gyBt1Y7WqKRPxoe+kdvRI0uBCeMt8DBpfev+l9GRCUH8QK2C7zl2J9VGzAQ39OBS/fc1hOmOqRczH3yll2Dpc6P61UMjnksIGuL9V51xYa3R1E0KW5Sh1vis9fCPbDiH5iuiSikibUM80Yug/QtTUTqUqkbGc+yCmIBijL9Lh+MSe/xCBWBiIrmpkY6k5ESgcuzuV3tc8/tgFdTrYCBh2KZ2JOOZXNc3KRsCQERJnUWGiWuMC64wxq7blar3WP1qmP2/3TKQwONT+FKH5f+rq9pNIcnr/yN9hsaHq7EkNrPgT4RBGsNxEbxjDLOCJ8+MDwU3OV+9qTP8BArk1NJ2jrow3cWQ35SmruqIaUkud675AEDwNoi+v339FfAswADLzuoci9jFaAAAAAElFTkSuQmCC) center center no-repeat; 15 | } -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // ========================================== 2 | // RECESS 3 | // INDEX: The root api definition 4 | // ========================================== 5 | // Copyright 2012 Twitter, Inc 6 | // Licensed under the Apache License v2.0 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // ========================================== 9 | 10 | 'use strict' 11 | 12 | // require core 13 | var RECESS = require('./core') 14 | , colors = require('colors') 15 | 16 | // define main export 17 | module.exports = function (paths, options, callback) { 18 | 19 | var option, message, i, instances = [] 20 | 21 | // if no options default to empty object 22 | options = options || {} 23 | 24 | // if options is a function, set to callback and set options to {} 25 | if (typeof options == 'function') (callback = options) && (options = {}) 26 | 27 | // if single path, convert to array 28 | if (typeof paths == 'string') paths = [paths] 29 | 30 | // there were no paths, show the docs 31 | if (!paths || !paths.length) return module.exports.docs() 32 | 33 | // if a compress flag is present, we automatically make compile flag true 34 | options.compress && (options.compile = true) 35 | 36 | // if format is set to compact, automatically set noSummary 37 | options.format && (options.format == 'compact') && (options.noSummary = true) 38 | 39 | // if not compiling, let user know which files will be linted 40 | if (!options.compile && options.cli && !options.noSummary) { 41 | message = "\nAnalyzing the following files: " + ((paths + '').replace(/,/g, ', ') + '\n').grey 42 | options.stripColors && (message = message.stripColors) 43 | console.log(message) 44 | } 45 | 46 | // for each path, create a new RECESS instance 47 | function recess(init, path, err) { 48 | if (path = paths.shift()) { 49 | return instances.push(new RECESS(path, options, recess)) 50 | } 51 | 52 | // map/filter for errors 53 | err = instances 54 | .map(function (i) { 55 | return i.errors.length && i.errors 56 | }) 57 | .filter(function (i) { 58 | return i 59 | }) 60 | 61 | // if no error, set explicitly to null 62 | err = err.length ? err[0] : null 63 | 64 | //callback 65 | callback && callback(err, instances) 66 | } 67 | 68 | // start processing paths 69 | recess(true) 70 | } 71 | 72 | // default options 73 | module.exports.DEFAULTS = RECESS.DEFAULTS = { 74 | compile: false 75 | , compress: false 76 | , config: false 77 | , format: 'text' 78 | , includePath: [] 79 | , noIDs: true 80 | , noJSPrefix: true 81 | , noOverqualifying: true 82 | , noSummary: false 83 | , noUnderscores: true 84 | , noUniversalSelectors: true 85 | , prefixWhitespace: true 86 | , strictPropertyOrder: true 87 | , stripColors: false 88 | , watch: false 89 | , zeroUnits: true 90 | , inlineImages: false 91 | } 92 | 93 | 94 | // expose RAW RECESS class 95 | module.exports.Constructor = RECESS 96 | 97 | // expose docs 98 | module.exports.docs = function () { 99 | console.log("\nGENERAL USE: " + "$".grey + " recess".cyan + " [path] ".yellow + "[options]\n".grey) 100 | console.log("OPTIONS:") 101 | for (var option in RECESS.DEFAULTS) console.log(' --' + option) 102 | console.log("\nEXAMPLE:\n\n" + " $".grey + " recess".cyan + " ./bootstrap.css ".yellow + "--noIDs false\n".grey) 103 | console.log('GENERAL HELP: ' + 'http://git.io/recess\n'.yellow) 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RECESS - NO LONGER MAINTAINED, DOES NOT WORK WITH NEWER LESS VERSIONS [![Build Status](https://secure.travis-ci.org/twitter/recess.png)](http://travis-ci.org/twitter/recess) 2 | ====== 3 | 4 | 5 | 6 | Developed at Twitter to support our internal styleguide, RECESS is a simple, attractive code quality tool for CSS built on top of LESS. 7 | 8 | Incorporate it into your development process as a linter, or integrate it directly into your build system as a compiler, RECESS will keep your source looking clean and super manageable. 9 | 10 | 11 | GENERAL USE 12 | ----------- 13 | 14 | ```CLI 15 | $ recess [path] [options] 16 | ``` 17 | 18 | OPTIONS 19 | ------- 20 | 21 | - --compile - compiles your code and outputs it to the terminal. Fixes white space and sort order. Can compile css or less. 22 | - --compress - compress your compiled code. 23 | - --config - accepts a path, which specifies a json config object 24 | - --format - control the output format of errors: 25 | - --format text - the default format, shows errors and context 26 | - --format compact - show errors one-error-per-line, useful for IDE integration 27 | - --noSummary - don't output the summary block for each file 28 | - --includePath - accepts an additional directory path to look for `@import`:ed LESS files in. 29 | - --stripColors - removes color from output (useful when logging) 30 | - --watch - watch filesystem for changes, useful when compiling Less projects 31 | - --noIDs - doesn't complain about using IDs in your stylesheets 32 | - --noJSPrefix - doesn't complain about styling `.js-` prefixed classnames 33 | - --noOverqualifying - doesn't complain about overqualified selectors (ie: `div#foo.bar`) 34 | - --noUnderscores - doesn't complain about using underscores in your class names 35 | - --noUniversalSelectors - doesn't complain about using the universal `*` selector 36 | - --prefixWhitespace - adds whitespace prefix to line up vender prefixed properties 37 | - --strictPropertyOrder - doesn't looking into your property ordering 38 | - --zeroUnits - doesn't complain if you add units to values of 0 39 | 40 | 41 | EXAMPLES 42 | -------- 43 | 44 | Lint all css files 45 | 46 | ```CLI 47 | $ recess *.css 48 | ``` 49 | 50 | Lint file, ignore styling of IDs 51 | 52 | ```CLI 53 | $ recess ./bootstrap.css --noIds false 54 | ``` 55 | 56 | Lint file with compact output and no color 57 | 58 | ```CLI 59 | $ recess ./bootstrap.css --format compact --stripColors 60 | ``` 61 | 62 | Compile and compress .less file, then output it to a new file 63 | 64 | ```CLI 65 | $ recess ./bootstrap.less --compress > ./bootstrap-production.css 66 | ``` 67 | 68 | Watch a directory for changes and auto compile a css file from the changes. *experimental* 69 | 70 | ```CLI 71 | $ recess input.less:ouput.css --watch watch/this/dir/for/changes 72 | ``` 73 | 74 | Watch a single file for changes and auto compile a css file from the changes. *experimental* 75 | 76 | ```CLI 77 | $ recess input.less:ouput.css --watch 78 | ``` 79 | 80 | PROGRAMMATIC API 81 | ---------------- 82 | 83 | Recess provides a pretty simple programmatic api. 84 | 85 | ```JS 86 | var recess = require('recess') 87 | ``` 88 | 89 | Once you've required recess, just pass it a `path` (or array of paths) and an optional `options` object and an optional `callback`: 90 | 91 | ```js 92 | recess(['../fat.css', '../twitter.css'], { compile: true }, callback) 93 | ``` 94 | 95 | The following options (and defaults) are available in the programatic api: 96 | 97 | - compile: false 98 | - compress: false 99 | - includePath: [] 100 | - noIDs: true 101 | - noJSPrefix: true 102 | - noOverqualifying: true 103 | - noUnderscores: true 104 | - noUniversalSelectors: true 105 | - prefixWhitespace: true 106 | - strictPropertyOrder: true 107 | - stripColors: false 108 | - zeroUnits: true 109 | 110 | The callback is fired when each instance has finished processessing an input. The callback is passed an array of of instances (one for each path). The instances have a bunch of useful things on them like the raw data and an array of output strings. 111 | 112 | When compiling, access the compiled source through the output property: 113 | 114 | ```js 115 | var recess = require('recess') 116 | 117 | recess('./js/fat.css', { compile: true }, function (err, obj) { 118 | if (err) throw err 119 | console.log( 120 | obj // recess instance for fat.css 121 | , obj.output // array of loggable content 122 | , obj.errors // array of failed lint rules 123 | ) 124 | }) 125 | ``` 126 | 127 | INSTALLATION 128 | ------------ 129 | 130 | To install recess you need both node and npm installed. 131 | 132 | ```CLI 133 | $ npm install recess -g 134 | ``` 135 | 136 | AUTHORS 137 | ------------ 138 | 139 | + **Jacob Thornton**: https://twitter.com/fat 140 | 141 | LICENSE 142 | ------------ 143 | 144 | Copyright 2012-2013 Twitter, Inc. 145 | 146 | Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 147 | -------------------------------------------------------------------------------- /test/compiled/preboot.css: -------------------------------------------------------------------------------- 1 | /* 2 | Bootstrap v1.1 3 | Variables and mixins to bootstrap any new web development project. 4 | */ 5 | 6 | /* Variables 7 | -------------------------------------------------- */ 8 | 9 | /* Mixins 10 | -------------------------------------------------- */ 11 | 12 | .clearfix { 13 | zoom: 1; 14 | } 15 | 16 | .clearfix:after { 17 | display: block; 18 | height: 0; 19 | clear: both; 20 | content: "."; 21 | visibility: hidden; 22 | } 23 | 24 | .center-block { 25 | display: block; 26 | margin: 0 auto; 27 | } 28 | 29 | .container { 30 | width: 940px; 31 | margin: 0 auto; 32 | zoom: 1; 33 | } 34 | 35 | .container:after { 36 | display: block; 37 | height: 0; 38 | clear: both; 39 | content: "."; 40 | visibility: hidden; 41 | } 42 | 43 | #flexbox .display-box { 44 | display: -moz-box; 45 | display: -webkit-box; 46 | display: box; 47 | } 48 | 49 | #reset .global-reset html, 50 | #reset .global-reset body, 51 | #reset .global-reset div, 52 | #reset .global-reset span, 53 | #reset .global-reset applet, 54 | #reset .global-reset object, 55 | #reset .global-reset iframe, 56 | #reset .global-reset h1, 57 | #reset .global-reset h2, 58 | #reset .global-reset h3, 59 | #reset .global-reset h4, 60 | #reset .global-reset h5, 61 | #reset .global-reset h6, 62 | #reset .global-reset p, 63 | #reset .global-reset blockquote, 64 | #reset .global-reset pre, 65 | #reset .global-reset a, 66 | #reset .global-reset abbr, 67 | #reset .global-reset acronym, 68 | #reset .global-reset address, 69 | #reset .global-reset big, 70 | #reset .global-reset cite, 71 | #reset .global-reset code, 72 | #reset .global-reset del, 73 | #reset .global-reset dfn, 74 | #reset .global-reset em, 75 | #reset .global-reset img, 76 | #reset .global-reset ins, 77 | #reset .global-reset kbd, 78 | #reset .global-reset q, 79 | #reset .global-reset s, 80 | #reset .global-reset samp, 81 | #reset .global-reset small, 82 | #reset .global-reset strike, 83 | #reset .global-reset strong, 84 | #reset .global-reset sub, 85 | #reset .global-reset sup, 86 | #reset .global-reset tt, 87 | #reset .global-reset var, 88 | #reset .global-reset b, 89 | #reset .global-reset u, 90 | #reset .global-reset i, 91 | #reset .global-reset center, 92 | #reset .global-reset dl, 93 | #reset .global-reset dt, 94 | #reset .global-reset dd, 95 | #reset .global-reset ol, 96 | #reset .global-reset ul, 97 | #reset .global-reset li, 98 | #reset .global-reset fieldset, 99 | #reset .global-reset form, 100 | #reset .global-reset label, 101 | #reset .global-reset legend, 102 | #reset .global-reset table, 103 | #reset .global-reset caption, 104 | #reset .global-reset tbody, 105 | #reset .global-reset tfoot, 106 | #reset .global-reset thead, 107 | #reset .global-reset tr, 108 | #reset .global-reset th, 109 | #reset .global-reset td, 110 | #reset .global-reset article, 111 | #reset .global-reset aside, 112 | #reset .global-reset canvas, 113 | #reset .global-reset details, 114 | #reset .global-reset embed, 115 | #reset .global-reset figure, 116 | #reset .global-reset figcaption, 117 | #reset .global-reset footer, 118 | #reset .global-reset header, 119 | #reset .global-reset hgroup, 120 | #reset .global-reset menu, 121 | #reset .global-reset nav, 122 | #reset .global-reset output, 123 | #reset .global-reset ruby, 124 | #reset .global-reset section, 125 | #reset .global-reset summary, 126 | #reset .global-reset time, 127 | #reset .global-reset mark, 128 | #reset .global-reset audio, 129 | #reset .global-reset video { 130 | padding: 0; 131 | margin: 0; 132 | font: inherit; 133 | font-size: 100%; 134 | vertical-align: baseline; 135 | border: 0; 136 | } 137 | 138 | #reset .global-reset body { 139 | line-height: 1; 140 | } 141 | 142 | #reset .global-reset ol, 143 | #reset .global-reset ul { 144 | list-style: none; 145 | } 146 | 147 | #reset .global-reset table { 148 | border-collapse: collapse; 149 | border-spacing: 0; 150 | } 151 | 152 | #reset .global-reset caption, 153 | #reset .global-reset th, 154 | #reset .global-reset td { 155 | font-weight: normal; 156 | text-align: left; 157 | vertical-align: middle; 158 | } 159 | 160 | #reset .global-reset q, 161 | #reset .global-reset blockquote { 162 | quotes: none; 163 | } 164 | 165 | #reset .global-reset q:before, 166 | #reset .global-reset blockquote:before, 167 | #reset .global-reset q:after, 168 | #reset .global-reset blockquote:after { 169 | content: ""; 170 | content: none; 171 | } 172 | 173 | #reset .global-reset a img { 174 | border: none; 175 | } 176 | 177 | #reset .global-reset article, 178 | #reset .global-reset aside, 179 | #reset .global-reset details, 180 | #reset .global-reset figcaption, 181 | #reset .global-reset figure, 182 | #reset .global-reset footer, 183 | #reset .global-reset header, 184 | #reset .global-reset hgroup, 185 | #reset .global-reset menu, 186 | #reset .global-reset nav, 187 | #reset .global-reset section { 188 | display: block; 189 | } 190 | 191 | #reset .reset-box-model { 192 | padding: 0; 193 | margin: 0; 194 | border: 0; 195 | } 196 | 197 | #reset .reset-font { 198 | font: inherit; 199 | font-size: 100%; 200 | vertical-align: baseline; 201 | } 202 | 203 | #reset .reset-focus { 204 | outline: 0; 205 | } 206 | 207 | #reset .reset-body { 208 | line-height: 1; 209 | } 210 | 211 | #reset .reset-list-style { 212 | list-style: none; 213 | } 214 | 215 | #reset .reset-table { 216 | border-collapse: collapse; 217 | border-spacing: 0; 218 | } 219 | 220 | #reset .reset-table-cell { 221 | font-weight: normal; 222 | text-align: left; 223 | vertical-align: middle; 224 | } 225 | 226 | #reset .reset-quotation { 227 | quotes: none; 228 | } 229 | 230 | #reset .reset-quotation:before, 231 | #reset .reset-quotation:after { 232 | content: ""; 233 | content: none; 234 | } 235 | 236 | #reset .reset-image-anchor-border { 237 | border: none; 238 | } 239 | 240 | #reset .reset-html5 article, 241 | #reset .reset-html5 aside, 242 | #reset .reset-html5 details, 243 | #reset .reset-html5 figcaption, 244 | #reset .reset-html5 figure, 245 | #reset .reset-html5 footer, 246 | #reset .reset-html5 header, 247 | #reset .reset-html5 hgroup, 248 | #reset .reset-html5 menu, 249 | #reset .reset-html5 nav, 250 | #reset .reset-html5 section { 251 | display: block; 252 | } -------------------------------------------------------------------------------- /lib/lint/strict-property-order.js: -------------------------------------------------------------------------------- 1 | // ========================================== 2 | // RECESS 3 | // RULE: Must use correct property ordering 4 | // ========================================== 5 | // Copyright 2012 Twitter, Inc 6 | // Licensed under the Apache License v2.0 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // ========================================== 9 | 10 | 'use strict' 11 | 12 | var _ = require('underscore') 13 | , util = require('../util') 14 | , RULE = { 15 | type: 'strictPropertyOrder' 16 | , message: 'Incorrect property order for rule' 17 | } 18 | 19 | // vendor prefix order 20 | , vendorPrefixes = [ 21 | '-webkit-' 22 | , '-khtml-' 23 | , '-epub-' 24 | , '-moz-' 25 | , '-ms-' 26 | , '-o-' 27 | ] 28 | 29 | // hack prefix order 30 | , hackPrefixes = [ 31 | '_' // ie7 32 | , '*' // ie6 33 | ] 34 | 35 | // css property order 36 | , order = [ 37 | 'position' 38 | , 'top' 39 | , 'right' 40 | , 'bottom' 41 | , 'left' 42 | , 'z-index' 43 | , 'display' 44 | , 'float' 45 | , 'width' 46 | , 'height' 47 | , 'max-width' 48 | , 'max-height' 49 | , 'min-width' 50 | , 'min-height' 51 | , 'padding' 52 | , 'padding-top' 53 | , 'padding-right' 54 | , 'padding-bottom' 55 | , 'padding-left' 56 | , 'margin' 57 | , 'margin-top' 58 | , 'margin-right' 59 | , 'margin-bottom' 60 | , 'margin-left' 61 | , 'margin-collapse' 62 | , 'margin-top-collapse' 63 | , 'margin-right-collapse' 64 | , 'margin-bottom-collapse' 65 | , 'margin-left-collapse' 66 | , 'overflow' 67 | , 'overflow-x' 68 | , 'overflow-y' 69 | , 'clip' 70 | , 'clear' 71 | , 'font' 72 | , 'font-family' 73 | , 'font-size' 74 | , 'font-smoothing' 75 | , 'osx-font-smoothing' 76 | , 'font-style' 77 | , 'font-weight' 78 | , 'hyphens' 79 | , 'src' 80 | , 'line-height' 81 | , 'letter-spacing' 82 | , 'word-spacing' 83 | , 'color' 84 | , 'text-align' 85 | , 'text-decoration' 86 | , 'text-indent' 87 | , 'text-overflow' 88 | , 'text-rendering' 89 | , 'text-size-adjust' 90 | , 'text-shadow' 91 | , 'text-transform' 92 | , 'word-break' 93 | , 'word-wrap' 94 | , 'white-space' 95 | , 'vertical-align' 96 | , 'list-style' 97 | , 'list-style-type' 98 | , 'list-style-position' 99 | , 'list-style-image' 100 | , 'pointer-events' 101 | , 'cursor' 102 | , 'background' 103 | , 'background-attachment' 104 | , 'background-color' 105 | , 'background-image' 106 | , 'background-position' 107 | , 'background-repeat' 108 | , 'background-size' 109 | , 'border' 110 | , 'border-collapse' 111 | , 'border-top' 112 | , 'border-right' 113 | , 'border-bottom' 114 | , 'border-left' 115 | , 'border-color' 116 | , 'border-image' 117 | , 'border-top-color' 118 | , 'border-right-color' 119 | , 'border-bottom-color' 120 | , 'border-left-color' 121 | , 'border-spacing' 122 | , 'border-style' 123 | , 'border-top-style' 124 | , 'border-right-style' 125 | , 'border-bottom-style' 126 | , 'border-left-style' 127 | , 'border-width' 128 | , 'border-top-width' 129 | , 'border-right-width' 130 | , 'border-bottom-width' 131 | , 'border-left-width' 132 | , 'border-radius' 133 | , 'border-top-right-radius' 134 | , 'border-bottom-right-radius' 135 | , 'border-bottom-left-radius' 136 | , 'border-top-left-radius' 137 | , 'border-radius-topright' 138 | , 'border-radius-bottomright' 139 | , 'border-radius-bottomleft' 140 | , 'border-radius-topleft' 141 | , 'content' 142 | , 'quotes' 143 | , 'outline' 144 | , 'outline-offset' 145 | , 'opacity' 146 | , 'filter' 147 | , 'visibility' 148 | , 'size' 149 | , 'zoom' 150 | , 'transform' 151 | , 'box-align' 152 | , 'box-flex' 153 | , 'box-orient' 154 | , 'box-pack' 155 | , 'box-shadow' 156 | , 'box-sizing' 157 | , 'table-layout' 158 | , 'animation' 159 | , 'animation-delay' 160 | , 'animation-duration' 161 | , 'animation-iteration-count' 162 | , 'animation-name' 163 | , 'animation-play-state' 164 | , 'animation-timing-function' 165 | , 'animation-fill-mode' 166 | , 'transition' 167 | , 'transition-delay' 168 | , 'transition-duration' 169 | , 'transition-property' 170 | , 'transition-timing-function' 171 | , 'background-clip' 172 | , 'backface-visibility' 173 | , 'resize' 174 | , 'appearance' 175 | , 'user-select' 176 | , 'interpolation-mode' 177 | , 'direction' 178 | , 'marks' 179 | , 'page' 180 | , 'set-link-source' 181 | , 'unicode-bidi' 182 | , 'speak' 183 | ] 184 | 185 | // regex tests 186 | , HACK_PREFIX = new RegExp('^(' + hackPrefixes.join('|').replace(/[-[\]{}()*+?.,\\^$#\s]/g, "\\$&") + ')') 187 | , VENDOR_PREFIX = new RegExp('^(' + vendorPrefixes.join('|').replace(/[-[\]{}()*+?.,\\^$#\s]/g, "\\$&") + ')') 188 | 189 | 190 | // validation method 191 | module.exports = function (def, data) { 192 | 193 | // // default validation to true 194 | var isValid = true 195 | , dict = {} 196 | , index = 0 197 | , cleanRules 198 | , sortedRules 199 | , firstLine 200 | , extract 201 | , selector 202 | 203 | // return if no rules to validate 204 | if (!def.rules) return isValid 205 | 206 | // recurse over nested rulesets 207 | def.rules.forEach(function (rule) { 208 | if (rule.selectors) module.exports(rule, data) 209 | }) 210 | 211 | cleanRules = def.rules.map(function (rule) { 212 | return rule.name && rule 213 | }).filter(function (item) { return item }) 214 | 215 | // sort rules 216 | sortedRules = _.sortBy(cleanRules, function (rule) { 217 | 218 | // pad value of each rule position to account for vendor prefixes 219 | var padding = (vendorPrefixes.length + 1) * 10 220 | , root 221 | , val 222 | 223 | // strip vendor prefix and hack prefix from rule name to find root 224 | root = rule.name 225 | .replace(VENDOR_PREFIX, '') 226 | .replace(HACK_PREFIX, '') 227 | 228 | // find value of order of the root css property 229 | val = order.indexOf(root) 230 | 231 | // if property is not found, exit with property not found error 232 | if (!~val) { 233 | return util.throwError(def, { 234 | type: 'propertyNotFound' 235 | , message: 'Unknown property name: "' + rule.name + '"' 236 | }) 237 | } 238 | 239 | // pad value 240 | val = (val * padding) + 10 241 | 242 | // adjust value based on prefix 243 | val += VENDOR_PREFIX.exec(rule.name) ? vendorPrefixes.indexOf(RegExp.$1) : (vendorPrefixes.length + 1) 244 | 245 | // adjust value based on css hack 246 | val += HACK_PREFIX.exec(rule.name) ? (hackPrefixes.indexOf(RegExp.$1)) : 0 247 | 248 | // return sort value 249 | return val 250 | }) 251 | 252 | // check to see if sortedRules has same order as provided rules 253 | isValid = _.isEqual(sortedRules, cleanRules) 254 | 255 | // return if sort is correct 256 | if (isValid) return isValid 257 | 258 | // get the line number of the first rule 259 | firstLine = util.getLine(def.rules[0].index, data) 260 | 261 | // generate a extract what the correct sorted rules would look like 262 | extract = sortedRules.map(function (rule) { 263 | if (!rule.name) return 264 | return util.padLine(firstLine + index++) 265 | + ' ' + rule.name + ': ' 266 | + (typeof rule.value == 'string' ? rule.value : rule.value.toCSS({})) 267 | + ';' 268 | }).filter(function (item) { return item }).join('\n') 269 | 270 | // extract selector for error message 271 | selector = (' "' + def.selectors.map(function (selector) { 272 | return selector.toCSS && selector.toCSS({}).replace(/^\s/, '') 273 | }).join(', ') + '"').magenta 274 | 275 | // set error object on defintion token 276 | util.throwError(def, { 277 | type: RULE.type 278 | , message: RULE.message + selector + '\n\n Correct order below:\n'.grey 279 | , extract: extract 280 | , sortedRules: sortedRules 281 | , line: firstLine 282 | }) 283 | 284 | // return valid state 285 | return isValid 286 | } 287 | -------------------------------------------------------------------------------- /lib/core.js: -------------------------------------------------------------------------------- 1 | // ========================================== 2 | // RECESS 3 | // CORE: The core class definition 4 | // ========================================== 5 | // Copyright 2012 Twitter, Inc 6 | // Licensed under the Apache License v2.0 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // ========================================== 9 | 10 | 'use strict' 11 | 12 | var _ = require('underscore') 13 | , colors = require('colors') 14 | , less = require('less') 15 | , util = require('./util') 16 | , path = require('path') 17 | , fs = require('fs') 18 | 19 | // core class defintion 20 | function RECESS(path, options, callback) { 21 | this.path = path 22 | this.output = [] 23 | this.errors = [] 24 | this.options = _.extend({}, RECESS.DEFAULTS, options) 25 | path && this.read() 26 | this.callback = callback 27 | } 28 | 29 | // instance methods 30 | RECESS.prototype = { 31 | 32 | constructor: RECESS 33 | 34 | , log: function (str, force) { 35 | 36 | if (this.options.stripColors) str = str.stripColors 37 | 38 | // if compiling only write with force flag 39 | if (!this.options.compile || force) { 40 | this.options.cli ? console.log(str) : this.output.push(str) 41 | } 42 | 43 | } 44 | 45 | , read: function () { 46 | var that = this 47 | 48 | // try to read data from path 49 | fs.readFile(this.path, 'utf8', function (err, data) { 50 | 51 | // if err, exit with could not read message 52 | if (err) { 53 | that.errors.push(err) 54 | that.log('Error reading file: '.red + String(that.path).grey + '\n', true) 55 | return that.callback && that.callback() 56 | } 57 | 58 | // set instance data 59 | that.data = data 60 | 61 | // parse data 62 | that.parse() 63 | 64 | }) 65 | } 66 | 67 | , parse: function () { 68 | var that = this 69 | , options = { 70 | paths: [path.dirname(this.path)].concat(this.options.includePath) 71 | , optimization: 0 72 | , filename: this.path && this.path.replace(/.*(?=\/)\//, '') 73 | } 74 | 75 | // try to parse with less parser 76 | try { 77 | 78 | // instantiate new parser with options 79 | new less.Parser(options) 80 | 81 | // parse data into tree 82 | .parse(this.data, function (err, tree) { 83 | 84 | if (err) { 85 | // push to errors array 86 | that.errors.push(err) 87 | 88 | if (err.type == 'Parse') { 89 | // parse error 90 | that.log("Parser error".red + (err.filename ? ' in ' + err.filename : '') + '\n') 91 | } else { 92 | // other exception 93 | that.log(String(err.name).red + ": " + err.message + ' of ' + String(err.filename).yellow + '\n') 94 | } 95 | 96 | // if extract - then log it 97 | err.extract && err.extract.forEach(function (line, index) { 98 | that.log(util.padLine(err.line + index) + line) 99 | }) 100 | 101 | // add extra line for readability after error log 102 | that.log(" ") 103 | 104 | // exit with callback if present 105 | return that.callback && that.callback() 106 | } 107 | 108 | // test to see if file has a less extension 109 | if (/less$/.test(that.path) && !that.parsed) { 110 | 111 | // if it's a LESS file, we flatten it 112 | that.data = tree.toCSS({}) 113 | 114 | // set parse to true so as to not infinitely reparse less files 115 | that.parsed = true 116 | 117 | // reparse less file 118 | return that.parse() 119 | } 120 | 121 | // set definitions to parse tree 122 | that.definitions = tree.rules 123 | 124 | // validation defintions 125 | that.options.compile ? that.compile() : that.validate() 126 | }) 127 | 128 | } catch (err) { 129 | 130 | // less exploded trying to parse the file (╯°□°)╯︵ ┻━┻ 131 | // push to errors array 132 | that.errors.push(err) 133 | 134 | // log a message trying to explain why 135 | that.log( 136 | "Parse error".red 137 | + ": " 138 | + err.message 139 | + " on line " 140 | + util.getLine(err.index, this.data) 141 | ) 142 | 143 | // exit with callback if present 144 | this.callback && this.callback() 145 | } 146 | } 147 | 148 | , compile: function () { 149 | var that = this 150 | , key 151 | , css 152 | 153 | // activate all relevant compilers 154 | Object.keys(this.options).forEach(function (key) { 155 | that.options[key] 156 | && RECESS.COMPILERS[key] 157 | && RECESS.COMPILERS[key].on.call(that) 158 | }) 159 | 160 | // iterate over defintions and compress them (join with new lines) 161 | css = this.definitions.map(function (def) { 162 | return def.toCSS([[]], { data: that.data, compress: that.options.compress }) 163 | }).join(this.options.compress ? '' : '\n') 164 | 165 | // minify with cssmin 166 | if (that.options.compress) css = require('./min').compressor.cssmin(css) 167 | 168 | // deactivate all relevant compilers 169 | Object.keys(this.options).reverse().forEach(function (key) { 170 | that.options[key] 171 | && RECESS.COMPILERS[key] 172 | && RECESS.COMPILERS[key].off() 173 | }) 174 | 175 | // cleanup trailing newlines 176 | css = css.replace(/[\n\s\r]*$/, '') 177 | 178 | // output css 179 | this.log(css, true) 180 | 181 | // callback and exit 182 | this.callback && this.callback() 183 | } 184 | 185 | , validate: function () { 186 | var failed 187 | , key 188 | 189 | // iterate over instance options 190 | for (key in this.options) { 191 | 192 | // if option has a validation, then we test it 193 | this.options[key] 194 | && RECESS.RULES[key] 195 | && !this.test(RECESS.RULES[key]) 196 | && (failed = true) 197 | 198 | } 199 | 200 | // exit with failed flag to validateStatus 201 | this.validateStatus(failed) 202 | } 203 | 204 | , test: function (validation) { 205 | var l = this.definitions.length 206 | , i = 0 207 | , isValid = true 208 | , rule 209 | , def 210 | , j 211 | , k 212 | 213 | // test each definition against a given validation 214 | for (; i < l; i++) { 215 | def = this.definitions[i] 216 | if (!validation(def, this.data)) isValid = false 217 | } 218 | 219 | // return valid state 220 | return isValid 221 | } 222 | 223 | , validateStatus: function (failed) { 224 | var that = this 225 | , fails 226 | , formatter 227 | 228 | if (failed) { 229 | 230 | // count errors 231 | fails = util.countErrors(this.definitions) 232 | 233 | if (!this.options.noSummary) { 234 | // log file overview 235 | this.log('FILE: ' + this.path.cyan) 236 | this.log('STATUS: ' + 'Busted'.magenta) 237 | this.log('FAILURES: ' + (fails + ' failure' + (fails > 1 ? 's' : '')).magenta + '\n') 238 | } 239 | 240 | if (this.options.format && this.options.format == 'compact') { 241 | formatter = function (err) { 242 | that.log(that.path + ':' + err.line + ':' + err.message) 243 | } 244 | } else { 245 | formatter = function (err) { 246 | that.log(err.message) 247 | err.extract && that.log(err.extract + '\n') 248 | } 249 | } 250 | 251 | // iterate through each definition 252 | this.definitions.forEach(function (def) { 253 | 254 | // if there's an error, log the error and optional err.extract 255 | def.errors 256 | && def.errors.length 257 | && def.errors.forEach(formatter) 258 | }) 259 | 260 | } else { 261 | // it was a success - let the user know! 262 | this.log('FILE: ' + this.path.cyan) 263 | this.log('STATUS: ' + 'Perfect!\n'.yellow) 264 | } 265 | 266 | // callback and exit 267 | this.callback && this.callback() 268 | } 269 | 270 | } 271 | 272 | // import validation rules 273 | RECESS.RULES = {} 274 | 275 | fs.readdirSync(path.join(__dirname, 'lint')).forEach(function (name) { 276 | var camelName = name 277 | .replace(/(\-[a-z])/gi, function ($1) { return $1.toUpperCase().replace('-', '') }) 278 | .replace(/\.js$/, '') 279 | RECESS.RULES[camelName] = require(path.join(__dirname, 'lint', name)) 280 | }) 281 | 282 | // import compilers 283 | RECESS.COMPILERS = {} 284 | 285 | fs.readdirSync(path.join(__dirname, 'compile')).forEach(function (name) { 286 | var camelName = name 287 | .replace(/(\-[a-z])/gi, function ($1) { return $1.toUpperCase().replace('-', '') }) 288 | .replace(/\.js$/, '') 289 | RECESS.COMPILERS[camelName] = require(path.join(__dirname, 'compile', name)) 290 | }) 291 | 292 | // export class 293 | module.exports = RECESS -------------------------------------------------------------------------------- /test/types/lint.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | , RECESS = require('../../lib') 3 | , colors = require('colors') 4 | , fs = require('fs') 5 | , noop = function () {} 6 | 7 | 8 | // LOGGING 9 | !function () { 10 | 11 | var log = console.log 12 | , loggedStr 13 | , withOutCompile 14 | , withCompile 15 | 16 | console.log = function (string) { loggedStr = string } 17 | 18 | withOutCompile = new RECESS.Constructor(null, { cli: true }) 19 | withOutCompile.log('first') 20 | assert.equal(loggedStr, 'first', 'console.log was not called when compile was true') 21 | withCompile = new RECESS.Constructor(false, { compile: true, cli: true}) 22 | withCompile.log('second') 23 | assert.equal(loggedStr, 'first', 'console.log was not called when compile was true') 24 | withCompile.log('third', true) 25 | assert.equal(loggedStr, 'third', 'console.log was called when force was was true') 26 | 27 | console.log = log 28 | 29 | }() 30 | 31 | //--stripColor 32 | !function () { 33 | var log = console.log 34 | , loggedStr 35 | , cliInstance 36 | 37 | console.log = function (string) { loggedStr = string } 38 | 39 | cliInstance = new RECESS.Constructor(null, { cli: true }) 40 | cliInstance.log('hello'.red) 41 | assert.equal(loggedStr, 'hello'.red, 'console.log was called with colored string') 42 | cliInstance = new RECESS.Constructor(null, { cli: true, stripColors: true }) 43 | cliInstance.log('hello'.red) 44 | assert.equal(loggedStr, 'hello', 'console.log was called with color stripped string') 45 | 46 | console.log = log 47 | }() 48 | 49 | 50 | //VALIDATIONS.strictPropertyOrder 51 | !function () { 52 | 53 | var path = 'test/fixtures/property-order.css' 54 | , Recess = new RECESS.Constructor() 55 | , validate = RECESS.Constructor.prototype.validate 56 | 57 | RECESS.Constructor.prototype.validate = noop 58 | 59 | Recess.data = fs.readFileSync(path, 'utf8') 60 | 61 | Recess.parse() 62 | 63 | RECESS.Constructor.RULES.strictPropertyOrder(Recess.definitions[0], Recess.data) 64 | 65 | assert.ok(Recess.definitions[0].errors) 66 | assert.equal(Recess.definitions[0].errors.length, 1, 'one error found') 67 | assert.equal(Recess.definitions[0].errors[0].type, 'strictPropertyOrder', 'strictPropertyOrder exception raised') 68 | assert.equal(Recess.definitions[0].errors[0].line, 5, 'Correct line number reported') 69 | assert.equal(Recess.definitions[0].errors[0].sortedRules.length, Recess.definitions[0].rules.length, 'same rule length in property') 70 | assert.equal(Recess.definitions[0].errors[0].sortedRules[0].name, 'position', 'Correctly ordered') 71 | assert.equal(Recess.definitions[0].errors[0].sortedRules[1].name, 'display', 'Correctly ordered') 72 | assert.equal(Recess.definitions[0].errors[0].sortedRules[2].name, 'font', 'Correctly ordered') 73 | assert.equal(Recess.definitions[0].errors[0].sortedRules[3].name, 'font-size', 'Correctly ordered') 74 | assert.equal(Recess.definitions[0].errors[0].sortedRules[4].name, 'color', 'Correctly ordered') 75 | assert.equal(Recess.definitions[0].errors[0].sortedRules[5].name, 'background', 'Correctly ordered') 76 | 77 | RECESS.Constructor.prototype.validate = validate 78 | 79 | }() 80 | 81 | 82 | //VALIDATIONS.noJSPrefix 83 | !function () { 84 | 85 | var path = 'test/fixtures/no-JS.css' 86 | , Recess = new RECESS.Constructor() 87 | , validate = RECESS.Constructor.prototype.validate 88 | , lines = [7, 8, 16, 16, 21, 21] 89 | 90 | RECESS.Constructor.prototype.validate = noop 91 | 92 | Recess.data = fs.readFileSync(path, 'utf8') 93 | 94 | Recess.parse() 95 | 96 | Recess.definitions.forEach(function (def) { 97 | 98 | RECESS.Constructor.RULES.noJSPrefix(def, Recess.data) 99 | 100 | assert.ok(def.errors) 101 | 102 | assert.equal(def.errors.length, 2, 'one error found') 103 | assert.equal(def.errors[0].type, 'noJSPrefix') 104 | assert.equal(def.errors[0].line, lines.shift(), 'Correct line number reported') 105 | assert.equal(def.errors[1].type, 'noJSPrefix') 106 | assert.equal(def.errors[1].line, lines.shift(), 'Correct line number reported') 107 | 108 | }) 109 | 110 | RECESS.Constructor.prototype.validate = validate 111 | 112 | }() 113 | 114 | 115 | //VALIDATIONS.noIDs 116 | !function () { 117 | 118 | var path = 'test/fixtures/no-IDs.css' 119 | , Recess = new RECESS.Constructor() 120 | , validate = RECESS.Constructor.prototype.validate 121 | , lines = [1, 7, 13] 122 | 123 | RECESS.Constructor.prototype.validate = noop 124 | 125 | Recess.data = fs.readFileSync(path, 'utf8') 126 | 127 | Recess.parse() 128 | 129 | Recess.definitions.forEach(function (def) { 130 | 131 | RECESS.Constructor.RULES.noIDs(def, Recess.data) 132 | 133 | assert.ok(def.errors) 134 | assert.equal(def.errors.length, 1, 'one error found') 135 | assert.equal(def.errors[0].type, 'noIDs') 136 | assert.equal(def.errors[0].line, lines.shift(), 'Correct line number reported') 137 | 138 | }) 139 | 140 | RECESS.Constructor.prototype.validate = validate 141 | 142 | }() 143 | 144 | //VALIDATIONS.noUnderscores 145 | !function () { 146 | 147 | var path = 'test/fixtures/no-underscores.css' 148 | , Recess = new RECESS.Constructor() 149 | , validate = RECESS.Constructor.prototype.validate 150 | , lines = [1, 6, 6] 151 | 152 | RECESS.Constructor.prototype.validate = noop 153 | 154 | Recess.data = fs.readFileSync(path, 'utf8') 155 | 156 | Recess.parse() 157 | 158 | Recess.definitions.forEach(function (def) { 159 | 160 | RECESS.Constructor.RULES.noUnderscores(def, Recess.data) 161 | 162 | assert.ok(def.errors) 163 | assert.equal(def.errors.length, 1, 'one error found') 164 | assert.equal(def.errors[0].type, 'noUnderscores') 165 | assert.equal(def.errors[0].line, lines.shift(), 'Correct line number reported') 166 | 167 | }) 168 | 169 | RECESS.Constructor.prototype.validate = validate 170 | 171 | }() 172 | 173 | //VALIDATIONS.universalSecltors 174 | !function () { 175 | 176 | var path = 'test/fixtures/universal-selectors.css' 177 | , Recess = new RECESS.Constructor() 178 | , validate = RECESS.Constructor.prototype.validate 179 | , counts = [1, 3, 1] 180 | , lines = [1, 5, 6, 7, 11] 181 | 182 | RECESS.Constructor.prototype.validate = noop 183 | 184 | Recess.data = fs.readFileSync(path, 'utf8') 185 | 186 | Recess.parse() 187 | 188 | Recess.definitions.forEach(function (def) { 189 | 190 | RECESS.Constructor.RULES.noUniversalSelectors(def, Recess.data) 191 | 192 | assert.ok(def.errors) 193 | assert.equal(def.errors.length, counts.shift(), 'Correct error count found') 194 | def.errors.forEach(function (error) { 195 | assert.equal(error.type, 'noUniversalSelectors') 196 | assert.equal(error.line, lines.shift(), 'Correct line number reported') 197 | }) 198 | }) 199 | 200 | RECESS.Constructor.prototype.validate = validate 201 | 202 | }() 203 | 204 | //VALIDATIONS.overQualifying 205 | !function () { 206 | 207 | var path = 'test/fixtures/no-overqualifying.css' 208 | , Recess = new RECESS.Constructor() 209 | , validate = RECESS.Constructor.prototype.validate 210 | , counts = [1, 2] 211 | , lines = [1, 7, 8] 212 | 213 | RECESS.Constructor.prototype.validate = noop 214 | 215 | Recess.data = fs.readFileSync(path, 'utf8') 216 | 217 | Recess.parse() 218 | 219 | Recess.definitions.forEach(function (def) { 220 | RECESS.Constructor.RULES.noOverqualifying(def, Recess.data) 221 | 222 | assert.ok(def.errors) 223 | assert.equal(def.errors.length, counts.shift(), 'Correct error count found') 224 | def.errors.forEach(function (error) { 225 | assert.equal(error.type, 'noOverqualifying') 226 | assert.equal(error.line, lines.shift(), 'Correct line number reported') 227 | }) 228 | }) 229 | 230 | RECESS.Constructor.prototype.validate = validate 231 | 232 | }() 233 | 234 | // Cannot read property 'red' of undefined 235 | !function () { 236 | 237 | var Recess = new RECESS.Constructor() 238 | , validate = RECESS.Constructor.prototype.validate 239 | 240 | RECESS.Constructor.prototype.validate = noop 241 | 242 | Recess.data = ".foo { background:green;; }" 243 | 244 | Recess.parse() 245 | 246 | assert.notEqual(Recess.output[0], '\u001b[31mParse error\u001b[39m: Cannot read property \'red\' of undefined on line 1'); 247 | 248 | RECESS.Constructor.prototype.validate = validate 249 | 250 | }() 251 | 252 | //VALIDATIONS.inlineImage 253 | !function () { 254 | 255 | var path = 'test/fixtures/inline-images.css' 256 | , Recess = new RECESS.Constructor() 257 | , validate = RECESS.Constructor.prototype.validate 258 | , counts = [1, 1, 1, 0] 259 | , lines = [2, 5, 8] 260 | 261 | RECESS.Constructor.prototype.validate = noop 262 | 263 | Recess.data = fs.readFileSync(path, 'utf8') 264 | 265 | Recess.parse() 266 | 267 | Recess.definitions.forEach(function (def) { 268 | RECESS.Constructor.RULES.inlineImages(def, Recess.data) 269 | 270 | if (counts[0]) { 271 | assert.ok(def.errors) 272 | assert.equal(def.errors.length, counts.shift(), 'Correct error count found') 273 | def.errors.forEach(function (error) { 274 | assert.equal(def.errors[0].type, 'inlineImages') 275 | assert.equal(error.line, lines.shift(), 'Correct line number reported') 276 | }) 277 | } else { 278 | assert.ok(!def.errors) 279 | } 280 | }) 281 | 282 | RECESS.Constructor.prototype.validate = validate 283 | 284 | }() 285 | 286 | // Keep order of input paths 287 | !function () { 288 | 289 | RECESS([ 290 | 'test/fixtures/blog.css', 291 | 'test/fixtures/inline-images.css' 292 | ], function (err, instance) { 293 | assert(instance[0].path === 'test/fixtures/blog.css'); 294 | }) 295 | 296 | }() 297 | 298 | 299 | console.log("✓ linting".green) 300 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /test/fixtures/preboot.less: -------------------------------------------------------------------------------- 1 | /* 2 | Bootstrap v1.1 3 | Variables and mixins to bootstrap any new web development project. 4 | */ 5 | 6 | 7 | /* Variables 8 | -------------------------------------------------- */ 9 | 10 | // Links 11 | @linkColor: #8b59c2; 12 | @linkColorHover: darken(@linkColor, 10); 13 | 14 | // Grays 15 | @white: #fff; 16 | @grayLighter: #ccc; 17 | @grayLight: #777; 18 | @gray: #555; 19 | @grayDark: #333; 20 | @black: #000; 21 | 22 | // Accent Colors 23 | @blue: #08b5fb; 24 | @green: #46a546; 25 | @red: #9d261d; 26 | @yellow: #ffc40d; 27 | @orange: #f89406; 28 | @pink: #c3325f; 29 | @purple: #7a43b6; 30 | 31 | // Baseline grid 32 | @baseline: 20px; 33 | 34 | // Griditude 35 | @gridColumns: 16; 36 | @gridColumnWidth: 40px; 37 | @gridGutterWidth: 20px; 38 | @siteWidth: (@gridColumns * @gridColumnWidth) + (@gridGutterWidth * (@gridColumns - 1)); 39 | 40 | 41 | /* Mixins 42 | -------------------------------------------------- */ 43 | 44 | // Clearfix for clearing floats like a boss 45 | .clearfix { 46 | zoom: 1; 47 | &:after { 48 | display: block; 49 | visibility: hidden; 50 | height: 0; 51 | clear: both; 52 | content: "."; 53 | } 54 | } 55 | 56 | // Center-align a block level element 57 | .center-block { 58 | display: block; 59 | margin: 0 auto; 60 | } 61 | 62 | // Sizing shortcuts 63 | .size(@height: 5px, @width: 5px) { 64 | height: @height; 65 | width: @width; 66 | } 67 | .square(@size: 5px) { 68 | .size(@size, @size); 69 | } 70 | 71 | // Input placeholder text 72 | .placeholder(@color: @grayLight) { 73 | :-moz-placeholder { 74 | color: @color; 75 | } 76 | ::-webkit-input-placeholder { 77 | color: @color; 78 | } 79 | } 80 | 81 | // Font Stacks 82 | #font { 83 | .shorthand(@weight: normal, @size: 14px, @lineHeight: 20px) { 84 | font-size: @size; 85 | font-weight: @weight; 86 | line-height: @lineHeight; 87 | } 88 | .sans-serif(@weight: normal, @size: 14px, @lineHeight: 20px) { 89 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 90 | font-size: @size; 91 | font-weight: @weight; 92 | line-height: @lineHeight; 93 | } 94 | .serif(@weight: normal, @size: 14px, @lineHeight: 20px) { 95 | font-family: "Georgia", Times New Roman, Times, sans-serif; 96 | font-size: @size; 97 | font-weight: @weight; 98 | line-height: @lineHeight; 99 | } 100 | .monospace(@weight: normal, @size: 12px, @lineHeight: 20px) { 101 | font-family: "Monaco", Courier New, monospace; 102 | font-size: @size; 103 | font-weight: @weight; 104 | line-height: @lineHeight; 105 | } 106 | } 107 | 108 | // Grid System 109 | .container { 110 | width: @siteWidth; 111 | margin: 0 auto; 112 | .clearfix(); 113 | } 114 | .columns(@columnSpan: 1) { 115 | display: inline; 116 | float: left; 117 | width: (@gridColumnWidth * @columnSpan) + (@gridGutterWidth * (@columnSpan - 1)); 118 | margin-left: @gridGutterWidth; 119 | &:first-child { 120 | margin-left: 0; 121 | } 122 | } 123 | .offset(@columnOffset: 1) { 124 | margin-left: (@gridColumnWidth * @columnOffset) + (@gridGutterWidth * (@columnOffset - 1)) !important; 125 | } 126 | 127 | // Border Radius 128 | .border-radius(@radius: 5px) { 129 | -moz-border-radius: @radius; 130 | border-radius: @radius; 131 | } 132 | 133 | // Drop shadows 134 | .box-shadow(@shadow: 0 1px 3px rgba(0,0,0,.25)) { 135 | -webkit-box-shadow: @shadow; 136 | -moz-box-shadow: @shadow; 137 | box-shadow: @shadow; 138 | } 139 | 140 | // Transitions 141 | .transition(@transition) { 142 | -webkit-transition: @transition; 143 | -moz-transition: @transition; 144 | transition: @transition; 145 | } 146 | 147 | // CSS3 Content Columns 148 | .content-columns(@columnCount, @columnGap: 20px) { 149 | -webkit-column-count: @columnCount; 150 | -webkit-column-gap: @columnGap; 151 | -moz-column-count: @columnCount; 152 | -moz-column-gap: @columnGap; 153 | column-count: @columnCount; 154 | column-gap: @columnGap; 155 | } 156 | 157 | // Buttons 158 | .button(@color: #f5f5f5, @textColor: #333, @textShadow: 0 1px 1px rgba(255,255,255,.75), @fontSize: 13px, @padding: 9px 15px 10px, @borderRadius: 6px) { 159 | display: inline-block; 160 | #gradient > .vertical(@color,darken(saturate(@color,10),10)); 161 | padding: @padding; 162 | text-shadow: @textShadow; 163 | color: @textColor; 164 | font-size: @fontSize; 165 | line-height: 20px; 166 | .border-radius(@borderRadius); 167 | @shadow: inset 0 1px 0 rgba(255,255,255,.2), inset 0 -1px 0 rgba(0,0,0,.2), 0 1px 2px rgba(0,0,0,.25); 168 | .box-shadow(@shadow); 169 | &:hover { 170 | background-position: 0 -15px; 171 | color: @textColor; 172 | text-decoration: none; 173 | } 174 | } 175 | 176 | // Add an alphatransparency value to any background or border color (via Elyse Holladay) 177 | #translucent { 178 | .background(@color: @white, @alpha: 1) { 179 | background-color: hsla(hue(@color), saturation(@color), lightness(@color), @alpha); 180 | } 181 | .border(@color: @white, @alpha: 1) { 182 | border-color: hsla(hue(@color), saturation(@color), lightness(@color), @alpha); 183 | background-clip: padding-box; 184 | } 185 | } 186 | 187 | // Gradients 188 | #gradient { 189 | .horizontal (@startColor: #555, @endColor: #333) { 190 | background-color: @endColor; 191 | background-repeat: repeat-x; 192 | background-image: -khtml-gradient(linear, left top, right top, from(@startColor), to(@endColor)); /* Konqueror */ 193 | background-image: -moz-linear-gradient(left, @startColor, @endColor); /* FF 3.6+ */ 194 | background-image: -ms-linear-gradient(left, @startColor, @endColor); /* IE10 */ 195 | background-image: -webkit-gradient(linear, left top, right top, color-stop(0%, @startColor), color-stop(100%, @endColor)); /* Safari 4+, Chrome 2+ */ 196 | background-image: -webkit-linear-gradient(left, @startColor, @endColor); /* Safari 5.1+, Chrome 10+ */ 197 | background-image: -o-linear-gradient(left, @startColor, @endColor); /* Opera 11.10 */ 198 | filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)",@startColor,@endColor)); /* IE6 & IE7 */ 199 | -ms-filter: %("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)",@startColor,@endColor); /* IE8+ */ 200 | background-image: linear-gradient(left, @startColor, @endColor); /* the standard */ 201 | } 202 | .vertical (@startColor: #555, @endColor: #333) { 203 | background-color: @endColor; 204 | background-repeat: repeat-x; 205 | background-image: -khtml-gradient(linear, left top, left bottom, from(@startColor), to(@endColor)); /* Konqueror */ 206 | background-image: -moz-linear-gradient(@startColor, @endColor); /* FF 3.6+ */ 207 | background-image: -ms-linear-gradient(@startColor, @endColor); /* IE10 */ 208 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, @startColor), color-stop(100%, @endColor)); /* Safari 4+, Chrome 2+ */ 209 | background-image: -webkit-linear-gradient(@startColor, @endColor); /* Safari 5.1+, Chrome 10+ */ 210 | background-image: -o-linear-gradient(@startColor, @endColor); /* Opera 11.10 */ 211 | filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",@startColor,@endColor)); /* IE6 & IE7 */ 212 | -ms-filter: %("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",@startColor,@endColor); /* IE8+ */ 213 | background-image: linear-gradient(@startColor, @endColor); /* the standard */ 214 | } 215 | .directional (@startColor: #555, @endColor: #333, @deg: 45deg) { 216 | background-color: @endColor; 217 | background-repeat: repeat-x; 218 | background-image: -moz-linear-gradient(@deg, @startColor, @endColor); /* FF 3.6+ */ 219 | background-image: -ms-linear-gradient(@deg, @startColor, @endColor); /* IE10 */ 220 | background-image: -webkit-linear-gradient(@deg, @startColor, @endColor); /* Safari 5.1+, Chrome 10+ */ 221 | background-image: -o-linear-gradient(@deg, @startColor, @endColor); /* Opera 11.10 */ 222 | background-image: linear-gradient(@deg, @startColor, @endColor); /* the standard */ 223 | } 224 | .vertical-three-colors(@startColor: #00b3ee, @midColor: #7a43b6, @colorStop: 0.5, @endColor: #c3325f) { 225 | background-color: @endColor; 226 | background-repeat: no-repeat; 227 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(@startColor), color-stop(@colorStop, @midColor), to(@endColor)); 228 | background-image: -webkit-linear-gradient(@startColor, color-stop(@colorStop, @midColor), @endColor); 229 | background-image: -moz-linear-gradient(@startColor, color-stop(@midColor, @colorStop), @endColor); 230 | } 231 | } 232 | 233 | 234 | // Opacity 235 | .opacity(@opacity: 100) { 236 | filter: e(%("alpha(opacity=%d)", @opacity)); 237 | -khtml-opacity: @op / 100; 238 | -moz-opacity: @op / 100; 239 | opacity: @op / 100; 240 | } 241 | 242 | // CSS3 Flexible Box Module 243 | #flexbox { 244 | // #flexbox > .display-box; must be used along with other flexbox mixins 245 | .display-box { 246 | display: -moz-box; 247 | display: -webkit-box; 248 | display: box; 249 | } 250 | // Box align [ start | end | center | baseline | stretch ] 251 | .box-align(@alignment: stretch) { 252 | -moz-box-align: @alignment; 253 | -webkit-box-align: @alignment; 254 | box-align: @alignment; 255 | } 256 | // Box direction [ normal | reverse | inherit ] 257 | .box-direction(@direction: normal) { 258 | -moz-box-direction: @direction; 259 | -webkit-box-direction: @direction; 260 | box-direction: @direction; 261 | } 262 | // Box flex [ integer ] 263 | .box-flex(@flex: 0) { 264 | -moz-box-flex: @flex; 265 | -webkit-box-flex: @flex; 266 | box-flex: @flex; 267 | } 268 | // Box flex group [ integer ] 269 | .box-flex-group(@group: 1) { 270 | -moz-box-flex-group: @group; 271 | -webkit-box-flex-group: @group; 272 | box-flex-group: @group; 273 | } 274 | // Box orientation [ horizontal | vertical | inline-axis | block-axis | inherit ] 275 | .box-orient(@orientation: horizontal) { 276 | -moz-box-orient: @orientation; 277 | -webkit-box-orient: @orientation; 278 | box-orient: @orientation; 279 | } 280 | // Box ordinal group [ integer ] 281 | .box-ordinal-group(@group: 1) { 282 | -moz-box-ordinal-group: @group; 283 | -webkit-box-ordinal-group:@group; 284 | box-flex-ordinal-group: @group; 285 | } 286 | // Box pack [ start | end | center | justify ] 287 | .box-pack(@pack: start) { 288 | -moz-box-pack: @pack; 289 | -webkit-box-pack: @pack; 290 | box-pack: @pack; 291 | } 292 | } 293 | 294 | // Based on Eric Meyers' reset 2.0 - http://meyerweb.com/eric/tools/css/reset/index.html 295 | // & the Compass Reset utility - http://compass-style.org/reference/compass/reset/utilities/ 296 | #reset { 297 | .global-reset { 298 | html, body, div, span, applet, object, iframe, 299 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 300 | a, abbr, acronym, address, big, cite, code, 301 | del, dfn, em, img, ins, kbd, q, s, samp, 302 | small, strike, strong, sub, sup, tt, var, 303 | b, u, i, center, 304 | dl, dt, dd, ol, ul, li, 305 | fieldset, form, label, legend, 306 | table, caption, tbody, tfoot, thead, tr, th, td, 307 | article, aside, canvas, details, embed, 308 | figure, figcaption, footer, header, hgroup, 309 | menu, nav, output, ruby, section, summary, 310 | time, mark, audio, video { 311 | #reset > .reset-box-model; 312 | #reset > .reset-font; 313 | } 314 | body { 315 | #reset > .reset-body; 316 | } 317 | ol, ul { 318 | #reset > .reset-list-style; 319 | } 320 | table { 321 | #reset > .reset-table; 322 | } 323 | caption, th, td { 324 | #reset > .reset-table-cell; 325 | } 326 | q, blockquote { 327 | #reset > .reset-quotation; 328 | } 329 | a img { 330 | #reset > .reset-image-anchor-border; 331 | } 332 | #reset > .reset-html5; 333 | } 334 | .reset-box-model { 335 | margin: 0; 336 | padding: 0; 337 | border: 0; 338 | } 339 | .reset-font { 340 | font-size: 100%; 341 | font: inherit; 342 | vertical-align: baseline; 343 | } 344 | .reset-focus { 345 | outline: 0; 346 | } 347 | .reset-body { 348 | line-height: 1; 349 | } 350 | .reset-list-style { 351 | list-style: none; 352 | } 353 | .reset-table { 354 | border-collapse: collapse; 355 | border-spacing: 0; 356 | } 357 | .reset-table-cell { 358 | text-align: left; 359 | font-weight: normal; 360 | vertical-align: middle; 361 | } 362 | .reset-quotation { 363 | quotes: none; 364 | &:before, &:after { 365 | content: ""; 366 | content: none; 367 | } 368 | } 369 | .reset-image-anchor-border { 370 | border: none; 371 | } 372 | .reset-html5 { 373 | article, aside, details, figcaption, figure, 374 | footer, header, hgroup, menu, nav, section { 375 | display: block; 376 | } 377 | } 378 | } -------------------------------------------------------------------------------- /lib/min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * cssmin.js 3 | * Author: Stoyan Stefanov - http://phpied.com/ 4 | * This is a JavaScript port of the CSS minification tool 5 | * distributed with YUICompressor, itself a port 6 | * of the cssmin utility by Isaac Schlueter - http://foohack.com/ 7 | * Permission is hereby granted to use the JavaScript version under the same 8 | * conditions as the YUICompressor (original YUICompressor note below). 9 | */ 10 | 11 | /* 12 | * YUI Compressor 13 | * http://developer.yahoo.com/yui/compressor/ 14 | * Author: Julien Lecomte - http://www.julienlecomte.net/ 15 | * Copyright (c) 2011 Yahoo! Inc. All rights reserved. 16 | * The copyrights embodied in the content of this file are licensed 17 | * by Yahoo! Inc. under the BSD (revised) open source license. 18 | */ 19 | var YAHOO = YAHOO || {}; 20 | YAHOO.compressor = YAHOO.compressor || {}; 21 | 22 | /** 23 | * Utility method to replace all data urls with tokens before we start 24 | * compressing, to avoid performance issues running some of the subsequent 25 | * regexes against large strings chunks. 26 | * 27 | * @private 28 | * @method _extractDataUrls 29 | * @param {String} css The input css 30 | * @param {Array} The global array of tokens to preserve 31 | * @returns String The processed css 32 | */ 33 | YAHOO.compressor._extractDataUrls = function (css, preservedTokens) { 34 | 35 | // Leave data urls alone to increase parse performance. 36 | var maxIndex = css.length - 1, 37 | appendIndex = 0, 38 | startIndex, 39 | endIndex, 40 | terminator, 41 | foundTerminator, 42 | sb = [], 43 | m, 44 | preserver, 45 | token, 46 | pattern = /url\(\s*(["']?)data\:/g; 47 | 48 | // Since we need to account for non-base64 data urls, we need to handle 49 | // ' and ) being part of the data string. Hence switching to indexOf, 50 | // to determine whether or not we have matching string terminators and 51 | // handling sb appends directly, instead of using matcher.append* methods. 52 | 53 | while ((m = pattern.exec(css)) !== null) { 54 | 55 | startIndex = m.index + 4; // "url(".length() 56 | terminator = m[1]; // ', " or empty (not quoted) 57 | 58 | if (terminator.length === 0) { 59 | terminator = ")"; 60 | } 61 | 62 | foundTerminator = false; 63 | 64 | endIndex = pattern.lastIndex - 1; 65 | 66 | while(foundTerminator === false && endIndex+1 <= maxIndex) { 67 | endIndex = css.indexOf(terminator, endIndex + 1); 68 | 69 | // endIndex == 0 doesn't really apply here 70 | if ((endIndex > 0) && (css.charAt(endIndex - 1) !== '\\')) { 71 | foundTerminator = true; 72 | if (")" != terminator) { 73 | endIndex = css.indexOf(")", endIndex); 74 | } 75 | } 76 | } 77 | 78 | // Enough searching, start moving stuff over to the buffer 79 | sb.push(css.substring(appendIndex, m.index)); 80 | 81 | if (foundTerminator) { 82 | token = css.substring(startIndex, endIndex); 83 | token = token.replace(/\s+/g, ""); 84 | preservedTokens.push(token); 85 | 86 | preserver = "url(___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___)"; 87 | sb.push(preserver); 88 | 89 | appendIndex = endIndex + 1; 90 | } else { 91 | // No end terminator found, re-add the whole match. Should we throw/warn here? 92 | sb.push(css.substring(m.index, pattern.lastIndex)); 93 | appendIndex = pattern.lastIndex; 94 | } 95 | } 96 | 97 | sb.push(css.substring(appendIndex)); 98 | 99 | return sb.join(""); 100 | }; 101 | 102 | /** 103 | * Utility method to compress hex color values of the form #AABBCC to #ABC. 104 | * 105 | * DOES NOT compress CSS ID selectors which match the above pattern (which would break things). 106 | * e.g. #AddressForm { ... } 107 | * 108 | * DOES NOT compress IE filters, which have hex color values (which would break things). 109 | * e.g. filter: chroma(color="#FFFFFF"); 110 | * 111 | * DOES NOT compress invalid hex values. 112 | * e.g. background-color: #aabbccdd 113 | * 114 | * @private 115 | * @method _compressHexColors 116 | * @param {String} css The input css 117 | * @returns String The processed css 118 | */ 119 | YAHOO.compressor._compressHexColors = function(css) { 120 | 121 | // Look for hex colors inside { ... } (to avoid IDs) and which don't have a =, or a " in front of them (to avoid filters) 122 | var pattern = /(\=\s*?["']?)?#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])(\}|[^0-9a-f{][^{]*?\})/gi, 123 | m, 124 | index = 0, 125 | isFilter, 126 | sb = []; 127 | 128 | while ((m = pattern.exec(css)) !== null) { 129 | 130 | sb.push(css.substring(index, m.index)); 131 | 132 | isFilter = m[1]; 133 | 134 | if (isFilter) { 135 | // Restore, maintain case, otherwise filter will break 136 | sb.push(m[1] + "#" + (m[2] + m[3] + m[4] + m[5] + m[6] + m[7])); 137 | } else { 138 | if (m[2].toLowerCase() == m[3].toLowerCase() && 139 | m[4].toLowerCase() == m[5].toLowerCase() && 140 | m[6].toLowerCase() == m[7].toLowerCase()) { 141 | 142 | // Compress. 143 | sb.push("#" + (m[3] + m[5] + m[7]).toLowerCase()); 144 | } else { 145 | // Non compressible color, restore but lower case. 146 | sb.push("#" + (m[2] + m[3] + m[4] + m[5] + m[6] + m[7]).toLowerCase()); 147 | } 148 | } 149 | 150 | index = pattern.lastIndex = pattern.lastIndex - m[8].length; 151 | } 152 | 153 | sb.push(css.substring(index)); 154 | 155 | return sb.join(""); 156 | }; 157 | 158 | YAHOO.compressor.cssmin = function (css, linebreakpos) { 159 | 160 | var startIndex = 0, 161 | endIndex = 0, 162 | i = 0, max = 0, 163 | preservedTokens = [], 164 | comments = [], 165 | token = '', 166 | totallen = css.length, 167 | placeholder = ''; 168 | 169 | css = this._extractDataUrls(css, preservedTokens); 170 | 171 | // collect all comment blocks... 172 | while ((startIndex = css.indexOf("/*", startIndex)) >= 0) { 173 | endIndex = css.indexOf("*/", startIndex + 2); 174 | if (endIndex < 0) { 175 | endIndex = totallen; 176 | } 177 | token = css.slice(startIndex + 2, endIndex); 178 | comments.push(token); 179 | css = css.slice(0, startIndex + 2) + "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + (comments.length - 1) + "___" + css.slice(endIndex); 180 | startIndex += 2; 181 | } 182 | 183 | // preserve strings so their content doesn't get accidentally minified 184 | css = css.replace(/("([^\\"]|\\.|\\)*")|('([^\\']|\\.|\\)*')/g, function (match) { 185 | var i, max, quote = match.substring(0, 1); 186 | 187 | match = match.slice(1, -1); 188 | 189 | // maybe the string contains a comment-like substring? 190 | // one, maybe more? put'em back then 191 | if (match.indexOf("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_") >= 0) { 192 | for (i = 0, max = comments.length; i < max; i = i + 1) { 193 | match = match.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", comments[i]); 194 | } 195 | } 196 | 197 | // minify alpha opacity in filter strings 198 | match = match.replace(/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/gi, "alpha(opacity="); 199 | 200 | preservedTokens.push(match); 201 | return quote + "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___" + quote; 202 | }); 203 | 204 | // strings are safe, now wrestle the comments 205 | for (i = 0, max = comments.length; i < max; i = i + 1) { 206 | 207 | token = comments[i]; 208 | placeholder = "___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___"; 209 | 210 | // ! in the first position of the comment means preserve 211 | // so push to the preserved tokens keeping the ! 212 | if (token.charAt(0) === "!") { 213 | preservedTokens.push(token); 214 | css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); 215 | continue; 216 | } 217 | 218 | // \ in the last position looks like hack for Mac/IE5 219 | // shorten that to /*\*/ and the next one to /**/ 220 | if (token.charAt(token.length - 1) === "\\") { 221 | preservedTokens.push("\\"); 222 | css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); 223 | i = i + 1; // attn: advancing the loop 224 | preservedTokens.push(""); 225 | css = css.replace("___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_" + i + "___", "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); 226 | continue; 227 | } 228 | 229 | // keep empty comments after child selectors (IE7 hack) 230 | // e.g. html >/**/ body 231 | if (token.length === 0) { 232 | startIndex = css.indexOf(placeholder); 233 | if (startIndex > 2) { 234 | if (css.charAt(startIndex - 3) === '>') { 235 | preservedTokens.push(""); 236 | css = css.replace(placeholder, "___YUICSSMIN_PRESERVED_TOKEN_" + (preservedTokens.length - 1) + "___"); 237 | } 238 | } 239 | } 240 | 241 | // in all other cases kill the comment 242 | css = css.replace("/*" + placeholder + "*/", ""); 243 | } 244 | 245 | 246 | // Normalize all whitespace strings to single spaces. Easier to work with that way. 247 | css = css.replace(/\s+/g, " "); 248 | 249 | // Remove the spaces before the things that should not have spaces before them. 250 | // But, be careful not to turn "p :link {...}" into "p:link{...}" 251 | // Swap out any pseudo-class colons with the token, and then swap back. 252 | css = css.replace(/(^|\})(([^\{:])+:)+([^\{]*\{)/g, function (m) { 253 | return m.replace(":", "___YUICSSMIN_PSEUDOCLASSCOLON___"); 254 | }); 255 | css = css.replace(/\s+([!{};:>+\(\)\],])/g, '$1'); 256 | css = css.replace(/___YUICSSMIN_PSEUDOCLASSCOLON___/g, ":"); 257 | 258 | // retain space for special IE6 cases 259 | css = css.replace(/:first-(line|letter)(\{|,)/g, ":first-$1 $2"); 260 | 261 | // no space after the end of a preserved comment 262 | css = css.replace(/\*\/ /g, '*/'); 263 | 264 | 265 | // If there is a @charset, then only allow one, and push to the top of the file. 266 | css = css.replace(/^(.*)(@charset "[^"]*";)/gi, '$2$1'); 267 | css = css.replace(/^(\s*@charset [^;]+;\s*)+/gi, '$1'); 268 | 269 | // Put the space back in some cases, to support stuff like 270 | // @media screen and (-webkit-min-device-pixel-ratio:0){ 271 | css = css.replace(/\band\(/gi, "and ("); 272 | 273 | 274 | // Remove the spaces after the things that should not have spaces after them. 275 | css = css.replace(/([!{}:;>+\(\[,])\s+/g, '$1'); 276 | 277 | // remove unnecessary semicolons 278 | css = css.replace(/;+\}/g, "}"); 279 | 280 | // Replace 0(px,em,%) with 0. 281 | css = css.replace(/([\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)/gi, "$1$2"); 282 | 283 | // Replace 0 0 0 0; with 0. 284 | css = css.replace(/:0 0 0 0(;|\})/g, ":0$1"); 285 | css = css.replace(/:0 0 0(;|\})/g, ":0$1"); 286 | css = css.replace(/:0 0(;|\})/g, ":0$1"); 287 | 288 | // Replace background-position:0; with background-position:0 0; 289 | // same for transform-origin 290 | css = css.replace(/(background-position|transform-origin|webkit-transform-origin|moz-transform-origin|o-transform-origin|ms-transform-origin):0(;|\})/gi, function(all, prop, tail) { 291 | return prop.toLowerCase() + ":0 0" + tail; 292 | }); 293 | 294 | // Replace 0.6 to .6, but only when preceded by : or a white-space 295 | css = css.replace(/(:|\s)0+\.(\d+)/g, "$1.$2"); 296 | 297 | // Shorten colors from rgb(51,102,153) to #336699 298 | // This makes it more likely that it'll get further compressed in the next step. 299 | css = css.replace(/rgb\s*\(\s*([0-9,\s]+)\s*\)/gi, function () { 300 | var i, rgbcolors = arguments[1].split(','); 301 | for (i = 0; i < rgbcolors.length; i = i + 1) { 302 | rgbcolors[i] = parseInt(rgbcolors[i], 10).toString(16); 303 | if (rgbcolors[i].length === 1) { 304 | rgbcolors[i] = '0' + rgbcolors[i]; 305 | } 306 | } 307 | return '#' + rgbcolors.join(''); 308 | }); 309 | 310 | // Shorten colors from #AABBCC to #ABC. 311 | css = this._compressHexColors(css); 312 | 313 | // border: none -> border:0 314 | css = css.replace(/(border|border-top|border-right|border-bottom|border-right|outline|background):none(;|\})/gi, function(all, prop, tail) { 315 | return prop.toLowerCase() + ":0" + tail; 316 | }); 317 | 318 | // shorter opacity IE filter 319 | css = css.replace(/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/gi, "alpha(opacity="); 320 | 321 | // Remove empty rules. 322 | css = css.replace(/[^\};\{\/]+\{\}/g, ""); 323 | 324 | if (linebreakpos >= 0) { 325 | // Some source control tools don't like it when files containing lines longer 326 | // than, say 8000 characters, are checked in. The linebreak option is used in 327 | // that case to split long lines after a specific column. 328 | startIndex = 0; 329 | i = 0; 330 | while (i < css.length) { 331 | i = i + 1; 332 | if (css[i - 1] === '}' && i - startIndex > linebreakpos) { 333 | css = css.slice(0, i) + '\n' + css.slice(i); 334 | startIndex = i; 335 | } 336 | } 337 | } 338 | 339 | // Replace multiple semi-colons in a row by a single one 340 | // See SF bug #1980989 341 | css = css.replace(/;;+/g, ";"); 342 | 343 | // restore preserved comments and strings 344 | for (i = 0, max = preservedTokens.length; i < max; i = i + 1) { 345 | css = css.replace("___YUICSSMIN_PRESERVED_TOKEN_" + i + "___", preservedTokens[i]); 346 | } 347 | 348 | // Trim the final string (for any leading or trailing white spaces) 349 | css = css.replace(/^\s+|\s+$/g, ""); 350 | 351 | return css; 352 | 353 | }; 354 | 355 | exports.compressor = YAHOO.compressor; --------------------------------------------------------------------------------