Permalink
Browse files

Initial commit

Signed-off-by: Chris Aniszczyk <zx@twitter.com>
  • Loading branch information...
0 parents commit 485ccc60a3c329eb3fc8d27bd9e98c90b3f9b4cc @fat-bot fat-bot committed with caniszczyk Apr 17, 2012
Showing with 7,037 additions and 0 deletions.
  1. +37 −0 .gitignore
  2. +176 −0 LICENSE
  3. +116 −0 README.md
  4. +3,979 −0 benchmark/benchmark.less
  5. +47 −0 benchmark/index.js
  6. +50 −0 bin/recess
  7. +98 −0 lib/compile/prefix-whitespace.js
  8. +47 −0 lib/compile/strict-property-order.js
  9. +42 −0 lib/compile/zero-units.js
  10. +272 −0 lib/core.js
  11. +93 −0 lib/index.js
  12. +61 −0 lib/lint/no-IDs.js
  13. +62 −0 lib/lint/no-JS-prefix.js
  14. +59 −0 lib/lint/no-overqualifying.js
  15. +62 −0 lib/lint/no-underscores.js
  16. +62 −0 lib/lint/no-universal-selectors.js
  17. +269 −0 lib/lint/strict-property-order.js
  18. +71 −0 lib/lint/zero-units.js
  19. +47 −0 lib/util.js
  20. +14 −0 makefile
  21. +14 −0 package.json
  22. +139 −0 test/compiled/blog.css
  23. +16 −0 test/compiled/no-IDs.css
  24. +21 −0 test/compiled/no-JS.css
  25. +10 −0 test/compiled/no-overqualifying.css
  26. +9 −0 test/compiled/no-underscores.css
  27. +252 −0 test/compiled/preboot.css
  28. +7 −0 test/compiled/prefixes.css
  29. +11 −0 test/compiled/property-order.css
  30. +4 −0 test/compiled/simple.css
  31. +13 −0 test/compiled/universal-selectors.css
  32. +16 −0 test/compiled/zero-units.css
  33. +139 −0 test/fixtures/blog.css
  34. +15 −0 test/fixtures/no-IDs.css
  35. +26 −0 test/fixtures/no-JS.css
  36. +10 −0 test/fixtures/no-overqualifying.css
  37. +9 −0 test/fixtures/no-underscores.css
  38. +378 −0 test/fixtures/preboot.less
  39. +7 −0 test/fixtures/prefixes.css
  40. +11 −0 test/fixtures/property-order.css
  41. +4 −0 test/fixtures/simple.css
  42. +13 −0 test/fixtures/universal-selectors.css
  43. +16 −0 test/fixtures/zero-units.css
  44. +4 −0 test/index.js
  45. +16 −0 test/types/compile.js
  46. +15 −0 test/types/errors.js
  47. +198 −0 test/types/lint.js
37 .gitignore
@@ -0,0 +1,37 @@
+# Numerous always-ignore extensions
+*.diff
+*.err
+*.orig
+*.log
+*.rej
+*.swo
+*.swp
+*.vi
+*~
+*.sass-cache
+
+# OS or Editor folders
+.DS_Store
+._*
+Thumbs.db
+.cache
+.project
+.settings
+.tmproj
+*.esproj
+nbproject
+*.sublime-project
+*.sublime-workspace
+
+# Komodo
+*.komodoproject
+.komodotools
+
+# Folders to ignore
+.hg
+.svn
+.CVS
+.idea
+
+#node modules
+node_modules
176 LICENSE
@@ -0,0 +1,176 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
116 README.md
@@ -0,0 +1,116 @@
+RECESS [![Build Status](https://secure.travis-ci.org/twitter/recess.png)](http://travis-ci.org/twitter/recess)
+======
+
+Developed at Twitter to support our internal styleguide, RECESS is a simple, attractive code quality tool for CSS built on top of LESS.
+
+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 managable.
+
+
+GENERAL USE
+-----------
+
+```CLI
+$ recess [path] [options]
+```
+
+OPTIONS
+-------
+
+- --compile - compiles your code and outputs it to the terminal. Fixes white space and sort order. Can compile css or less.
+- --compress - compress your compiled code.
+- --config - accepts a path, which specifies a json config object
+- --noIDs - doesn't complain about using IDs in your stylesheets
+- --noJSPrefix - doesn't complain about styling `.js-` prefixed classnames
+- --noOverqualifying - doesn't complain about overqualified selectors (ie: `div#foo.bar`)
+- --noUnderscores - doesn't complain about using semicolons in your class names
+- --noUniversalSelectors - doesn't complain about using the universal `*` selector
+- --strictPropertyOrder - doesn't looking into your property ordering
+- --zeroUnits - doesn't complain if you add units to values of 0
+
+
+EXAMPLES
+--------
+
+Lint all css files
+
+```CLI
+$ recess *.css
+```
+
+Lint file, ignore styling of IDs
+
+```CLI
+$ recess ./bootstrap.css --noIds false
+```
+
+Compile and compress .less file, then output it to a new file
+
+```CLI
+$ recess ./bootstrap.less --compress > ./bootstrap-production.css
+```
+
+PROGRAMMATIC API
+----------------
+
+Recess provides a pretty simple programmatic api.
+
+```JS
+var recess = require('recess')
+```
+
+Once you've required recess, just pass it a `path` (or array of paths) and an optional `options` object and an optional `callback`:
+
+```js
+recess(['../fat.css', '../twitter.css'], { compile: true }, callback)
+```
+
+The following options (and defaults) are available in the programatic api:
+
+- compile: false
+- compress: false
+- noIDs: true
+- noJSPrefix: true
+- noOverqualifying: true
+- noUnderscores: true
+- noUniversalSelectors: true
+- prefixWhitespace: true
+- strictPropertyOrder: true
+- zeroUnits: true
+
+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.
+
+When compiling, access the compiled source through the output property:
+
+```js
+var recess = require('recess')
+
+recess('./js/fat.css', { compile: true }, function (err, obj) {
+ if (err) throw err
+ console.log(
+ obj // recess instance for fat.css
+ , obj.output // array of loggable content
+ , obj.errors // array of failed lint rules
+ )
+})
+```
+
+INSTALLATION
+------------
+
+To install recess you need both node and npm installed.
+
+```CLI
+$ npm install recess -g
+```
+
+AUTHORS
+------------
+
++ **Jacob Thornton**: https://twitter.com/fat
+
+LICENSE
+------------
+
+Copyright 2012 Twitter, Inc.
+
+Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
3,979 benchmark/benchmark.less
3,979 additions, 0 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
47 benchmark/index.js
@@ -0,0 +1,47 @@
+#!/usr/bin/env node
+var path = require('path')
+ , fs = require('fs')
+ , less = require('less')
+ , recess = require('../lib')
+ , file = path.join(__dirname, 'benchmark.less')
+
+fs.readFile(file, 'utf8', function (e, data) {
+ var css, start, end
+
+ new(less.Parser)({ optimization: 2 }).parse(data, function (err, tree) {
+
+ start = new Date()
+ css = tree.toCSS()
+ end = new Date()
+
+ console.log(
+ " LESS toCSS: "
+ + (end - start)
+ + " ms ("
+ + parseInt(1000 / (end - start) * data.length / 1024)
+ + " KB\/s)"
+ )
+
+ new(less.Parser)({ optimization: 2 }).parse(css, function (err, tree) {
+
+ var play = new recess.Constructor(false)
+
+ play.data = css
+ play.definitions = tree.rules
+ play.path = 'less'
+ play.callback = function () {
+ end = new Date()
+ console.log(
+ "RECESS toCSS: "
+ + (end - start)
+ + " ms ("
+ + parseInt(1000 / (end - start) * data.length / 1024)
+ + " KB\/s)"
+ )
+ }
+ start = new Date()
+ play.compile()
+ })
+
+ })
+})
50 bin/recess
@@ -0,0 +1,50 @@
+#!/usr/bin/env node
+var RECESS = require('../lib')
+ , nopt = require('nopt')
+ , path = require('path')
+ , fs = require('fs')
+ , config = '.recessrc'
+ , options
+ , paths
+
+// exit with docs
+if (process.argv.length == 2) return RECESS.docs()
+
+// define expected options
+options = {
+ compile: Boolean
+, compress: Boolean
+, config: path
+, noIDs: Boolean
+, noJSPrefix: Boolean
+, noOverqualifying: Boolean
+, noUnderscores: Boolean
+, noUniversalSelectors: Boolean
+, prefixWhitespace: Boolean
+, strictPropertyOrder: Boolean
+, zeroUnits: Boolean
+}
+
+// parse options
+options = nopt(options, {}, process.argv)
+
+// if help exit
+if (options.help) return RECESS.docs()
+
+// set path from remaining arguments
+paths = options.argv.remain
+
+// clean options object
+delete options.argv
+
+// check for config or default .recessrc
+if (options.config || path.existsSync(config)) {
+ config = JSON.parse(fs.readFileSync(options.config || config))
+ for (i in options) config[i] = options[i]
+ options = config
+}
+
+// set CLI to true
+options.cli = true
+
+RECESS(paths, options)
98 lib/compile/prefix-whitespace.js
@@ -0,0 +1,98 @@
+// ==========================================
+// RECESS
+// COMPILE: whitespace for vendor prefixes
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under the Apache License v2.0
+// http://www.apache.org/licenses/LICENSE-2.0
+// ==========================================
+
+'use strict'
+
+var less = require('less')
+ , toCSS
+
+ // vendor prfixes
+ , vendorPrefixes = [
+ '-webkit-'
+ , '-khtml-'
+ , '-epub-'
+ , '-moz-'
+ , '-ms-'
+ , '-o-'
+ ]
+ , VENDOR_PREFIX = new RegExp('^(\\s*(?:' + vendorPrefixes.join('|').replace(/[-[\]{}()*+?.,\\^$#\s]/g, "\\$&") + '))')
+
+
+// space defintion
+function space(rules, i) {
+ var rule = rules[i]
+ , j = i - 1
+ , peek = rules[j]
+ , result = ''
+ , ruleRoot
+ , peekRoot
+ , ruleVal
+ , peekVal
+
+ // skip if not peak, rule, or rule.name
+ if (!peek || !rule || !rule.name) return
+
+ // if previous rule is not a css property, try searching up tree for nearest rule
+ while (!peek.name) {
+ peek = rules[j--]
+
+ // if none, then exit
+ if (!peek) return
+ }
+
+ // check to see if name has a vnedor prefix
+ if (VENDOR_PREFIX.test(peek.name)) {
+
+ // strip vendor prefix from rule and prior rule
+ ruleRoot = rule.name.replace(VENDOR_PREFIX, '')
+ peekRoot = peek.name.replace(VENDOR_PREFIX, '')
+
+
+ // if they share the same root calculte the offset in spacing
+ if (ruleRoot === peekRoot) {
+
+ // calculate the rules val
+ ruleVal = rule.name.match(VENDOR_PREFIX)
+ ruleVal = (ruleVal && ruleVal[0].length) || 0
+
+ // calculate the peeks val
+ peekVal = peek.name.match(VENDOR_PREFIX)
+ peekVal = (peekVal && peekVal[0].length) || 0
+
+ // if peek has a value, offset the rule val
+ if (peekVal) {
+ ruleVal = peekVal - ruleVal
+ while (ruleVal--) result += ' '
+ }
+
+ }
+ }
+
+ // prefix the rule with the white space offset
+ rule.name = result + rule.name
+}
+
+function compile (context, env) {
+ // iterate over rules and space each property
+ for (var i = 0; i < this.rules.length; i++) {
+ space(this.rules, i)
+ }
+
+ // apply to base CSS
+ return toCSS.apply(this, arguments)
+}
+
+module.exports.on = function () {
+ toCSS = less.tree.Ruleset.prototype.toCSS
+ less.tree.Ruleset.prototype.toCSS = compile
+}
+
+module.exports.off = function () {
+ less.tree.Ruleset.prototype.toCSS = toCSS
+}
47 lib/compile/strict-property-order.js
@@ -0,0 +1,47 @@
+// ==========================================
+// RECESS
+// COMPILE: automatically sort properties
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under the Apache License v2.0
+// http://www.apache.org/licenses/LICENSE-2.0
+// ==========================================
+
+'use strict'
+
+var less = require('less')
+ , order = require('../lint/strict-property-order')
+ , toCSS
+
+
+function compile (context, env) {
+ var l
+
+ // test property order
+ order(this, env.data)
+
+ // search errors for sortedRules property
+ if (this.errors) {
+ for (l = this.errors.length; l--;) {
+
+ // if sorted rule found apply it, then exit
+ if (this.errors[l].sortedRules) {
+ this.rules = this.errors[l].sortedRules
+ break
+ }
+
+ }
+ }
+
+ // apply old toCSS method to updated object
+ return toCSS.apply(this, arguments)
+}
+
+module.exports.on = function () {
+ toCSS = less.tree.Ruleset.prototype.toCSS
+ less.tree.Ruleset.prototype.toCSS = compile
+}
+
+module.exports.off = function () {
+ less.tree.Ruleset.prototype.toCSS = toCSS
+}
42 lib/compile/zero-units.js
@@ -0,0 +1,42 @@
+// ==========================================
+// RECESS
+// COMPILE: remove units from 0 values
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under the Apache License v2.0
+// http://www.apache.org/licenses/LICENSE-2.0
+// ==========================================
+
+'use strict'
+
+var less = require('less')
+ , toCSS
+ , units = [
+ '%'
+ , 'in'
+ , 'cm'
+ , 'mm'
+ , 'em'
+ , 'ex'
+ , 'pt'
+ , 'pc'
+ , 'px'
+ ]
+ , UNITS = new RegExp('\\b0\\s?(' + units.join('|') + ')')
+
+function compile () {
+ // strip units from 0 values
+ var props = toCSS.apply(this, arguments)
+
+ // don't strip chars from hex codes
+ return /#/.test(props) ? props : props.replace(UNITS, '0')
+}
+
+module.exports.on = function () {
+ toCSS = less.tree.Value.prototype.toCSS
+ less.tree.Value.prototype.toCSS = compile
+}
+
+module.exports.off = function () {
+ less.tree.Value.prototype.toCSS = toCSS
+}
272 lib/core.js
@@ -0,0 +1,272 @@
+// ==========================================
+// RECESS
+// CORE: The core class definition
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under the Apache License v2.0
+// http://www.apache.org/licenses/LICENSE-2.0
+// ==========================================
+
+'use strict'
+
+var _ = require('underscore')
+ , colors = require('colors')
+ , less = require('less')
+ , util = require('./util')
+ , path = require('path')
+ , fs = require('fs')
+
+// core class defintion
+function RECESS(path, options, callback) {
+ this.path = path
+ this.output = []
+ this.errors = []
+ this.options = _.extend({}, RECESS.DEFAULTS, options)
+ path && this.read()
+ this.callback = callback
+}
+
+// instance methods
+RECESS.prototype = {
+
+ constructor: RECESS
+
+, log: function (str, force) {
+
+ // if compiling only write with force flag
+ if (!this.options.compile || force) {
+ this.options.cli ? console.log(str) : this.output.push(str)
+ }
+
+ }
+
+, read: function () {
+ var that = this
+
+ // try to read data from path
+ fs.readFile(this.path, 'utf8', function (err, data) {
+
+ // if err, exit with could not read message
+ if (err) {
+ that.errors.push(err)
+ that.log('Error reading file: '.red + that.path.grey + '\n', true)
+ return that.callback && that.callback()
+ }
+
+ // set instance data
+ that.data = data
+
+ // parse data
+ that.parse()
+
+ })
+ }
+
+, parse: function () {
+ var that = this
+ , options = {
+ paths: [path.dirname(this.path)]
+ , optimization: 0
+ , filename: this.path && this.path.replace(/.*(?=\/)\//, '')
+ }
+
+ // try to parse with less parser
+ try {
+
+ // instantiate new parser with options
+ new less.Parser(options)
+
+ // parse data into tree
+ .parse(this.data, function (err, tree) {
+
+ if (err) {
+ // push to errors array
+ that.errors.push(err)
+
+ // less gave up trying to parse the data ;_;
+ that.log(err.name.red + ": " + err.message + ' of ' + err.filename.yellow + '\n')
+
+ // if extract - then log it
+ err.extract && err.extract.forEach(function (line, index) {
+ that.log(util.padLine(err.line + index) + line)
+ })
+
+ // add extra line for readability after error log
+ that.log(" ")
+
+ // exit with callback if present
+ return that.callback && that.callback()
+ }
+
+ // test to see if file has a less extension
+ if (/less$/.test(that.path) && !that.parsed) {
+
+ // if it's a LESS file, we flatten it
+ that.data = tree.toCSS({})
+
+ // set parse to true so as to not infinitely reparse less files
+ that.parsed = true
+
+ // reparse less file
+ return that.parse()
+ }
+
+ // set definitions to parse tree
+ that.definitions = tree.rules
+
+ // validation defintions
+ that.options.compile ? that.compile() : that.validate()
+ })
+
+ } catch (err) {
+
+ // less exploded trying to parse the file (╯°□°)╯︵ ┻━┻
+ // push to errors array
+ that.errors.push(err)
+
+ // log a message trying to explain why
+ that.log(
+ "Parse errror".red
+ + ": "
+ + err.message
+ + " on line "
+ + util.getLine(err.index, this.data)
+ )
+
+ // exit with callback if present
+ this.callback && this.callback()
+ }
+ }
+
+, compile: function () {
+ var that = this
+ , key
+ , css
+
+ // activate all relevant compilers
+ Object.keys(this.options).forEach(function (key) {
+ that.options[key]
+ && RECESS.COMPILERS[key]
+ && RECESS.COMPILERS[key].on()
+ })
+
+ // iterate over defintions and compress them (join with new lines)
+ css = this.definitions.map(function (def) {
+ return def.toCSS([[]], { data: that.data, compress: that.options.compress })
+ }).join(this.options.compress ? '' : '\n')
+
+ // deactivate all relevant compilers
+ Object.keys(this.options).reverse().forEach(function (key) {
+ that.options[key]
+ && RECESS.COMPILERS[key]
+ && RECESS.COMPILERS[key].off()
+ })
+
+ // cleanup trailing newlines
+ css = css.replace(/[\n\s\r]*$/, '')
+
+ // output css
+ this.log(css, true)
+
+ // callback and exit
+ this.callback && this.callback()
+ }
+
+, validate: function () {
+ var failed
+ , key
+
+ // iterate over instance options
+ for (key in this.options) {
+
+ // if option has a validation, then we test it
+ this.options[key]
+ && RECESS.RULES[key]
+ && !this.test(RECESS.RULES[key])
+ && (failed = true)
+
+ }
+
+ // exit with failed flag to validateStatus
+ this.validateStatus(failed)
+ }
+
+, test: function (validation) {
+ var l = this.definitions.length
+ , i = 0
+ , isValid = true
+ , rule
+ , def
+ , j
+ , k
+
+ // test each definition against a given validation
+ for (; i < l; i++) {
+ def = this.definitions[i]
+ if (!validation(def, this.data)) isValid = false
+ }
+
+ // return valid state
+ return isValid
+ }
+
+, validateStatus: function (failed) {
+ var that = this
+ , fails
+
+ if (failed) {
+
+ // count errors
+ fails = util.countErrors(this.definitions)
+
+ // log file overview
+ this.log('FILE: ' + this.path.cyan)
+ this.log('STATUS: ' + 'Busted'.magenta)
+ this.log('FAILURES: ' + (fails + ' failure' + (fails > 1 ? 's' : '')).magenta + '\n')
+
+ // iterate through each definition
+ this.definitions.forEach(function (def) {
+
+ // if there's an error, log the error and optional err.extract
+ def.errors
+ && def.errors.length
+ && def.errors.forEach(function (err) {
+ that.log(err.message)
+ err.extract && that.log(err.extract + '\n')
+ })
+ })
+
+ } else {
+ // it was a success - let the user know!
+ this.log('FILE: ' + this.path.cyan)
+ this.log('STATUS: ' + 'Perfect!\n'.yellow)
+ }
+
+ // callback and exit
+ this.callback && this.callback()
+ }
+
+}
+
+// import validation rules
+RECESS.RULES = {}
+
+fs.readdirSync(path.join(__dirname, 'lint')).forEach(function (name) {
+ var camelName = name
+ .replace(/(\-[a-z])/gi, function ($1) { return $1.toUpperCase().replace('-', '') })
+ .replace(/\.js$/, '')
+ RECESS.RULES[camelName] = require(path.join(__dirname, 'lint', name))
+})
+
+// import compilers
+RECESS.COMPILERS = {}
+
+fs.readdirSync(path.join(__dirname, 'compile')).forEach(function (name) {
+ var camelName = name
+ .replace(/(\-[a-z])/gi, function ($1) { return $1.toUpperCase().replace('-', '') })
+ .replace(/\.js$/, '')
+ RECESS.COMPILERS[camelName] = require(path.join(__dirname, 'compile', name))
+})
+
+// export class
+module.exports = RECESS
93 lib/index.js
@@ -0,0 +1,93 @@
+// ==========================================
+// RECESS
+// INDEX: The root api definition
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under the Apache License v2.0
+// http://www.apache.org/licenses/LICENSE-2.0
+// ==========================================
+
+'use strict'
+
+// require core
+var RECESS = require('./core')
+ , colors = require('colors')
+
+// define main export
+module.exports = function (paths, options, callback) {
+
+ var option, i, instances = []
+
+ // if no options default to empty object
+ options = options || {}
+
+ // if options is a function, set to callback and set options to {}
+ if (typeof options == 'function') (callback = options) && (options = {})
+
+ // if single path, convert to array
+ if (typeof paths == 'string') paths = [paths]
+
+ // there were no paths, show the docs
+ if (!paths || !paths.length) return module.exports.docs()
+
+ // if a compress flag is present, we automatically make compile flag true
+ options.compress && (options.compile = true)
+
+ // if not compiling, let user know which files will be linted
+ if (!options.compile && options.cli) {
+ console.log("\nAnalyzing the following files: " + ((paths + '').replace(/,/g, ', ') + '\n').grey)
+ }
+
+ // for each path, create a new RECESS instance
+ function recess(init, path, err) {
+ if (path = paths.pop()) {
+ return instances.push(new RECESS(path, options, recess))
+ }
+
+ // map/filter for errors
+ err = instances
+ .map(function (i) {
+ return i.errors.length && i.errors
+ })
+ .filter(function (i) {
+ return i
+ })
+
+ // if no error, set explicitly to null
+ err = err.length ? err[0] : null
+
+ //callback
+ callback && callback(err, instances.length > 1 ? instances : instances[0])
+ }
+
+ // start processing paths
+ recess(true)
+}
+
+// default options
+module.exports.DEFAULTS = RECESS.DEFAULTS = {
+ compile: false
+, compress: false
+, config: false
+, noIDs: true
+, noJSPrefix: true
+, noOverqualifying: true
+, noUnderscores: true
+, noUniversalSelectors: true
+, prefixWhitespace: true
+, strictPropertyOrder: true
+, zeroUnits: true
+}
+
+
+// expose RAW RECESS class
+module.exports.Constructor = RECESS
+
+// expose docs
+module.exports.docs = function () {
+ console.log("\nGENERAL USE: " + "$".grey + " recess".cyan + " [path] ".yellow + "[options]\n".grey)
+ console.log("OPTIONS:")
+ for (var option in RECESS.DEFAULTS) console.log(' --' + option)
+ console.log("\nEXAMPLE:\n\n" + " $".grey + " recess".cyan + " ./bootstrap.css ".yellow + "--noIDs false\n".grey)
+ console.log('GENERAL HELP: ' + 'http://git.io/recess\n'.yellow)
+}
61 lib/lint/no-IDs.js
@@ -0,0 +1,61 @@
+// ==========================================
+// RECESS
+// RULE: Id's should not be styled
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under the Apache License v2.0
+// http://www.apache.org/licenses/LICENSE-2.0
+// ==========================================
+
+'use strict'
+
+var util = require('../util')
+ , RULE = {
+ type: 'noIDs'
+ , exp: /^#/
+ , message: 'Id\'s should not be styled'
+ }
+
+// validation method
+module.exports = function (def, data) {
+
+ // default validation to true
+ var isValid = true
+
+ // return if no selectors to validate
+ if (!def.selectors) return isValid
+
+ // loop over selectors
+ def.selectors.forEach(function (selector) {
+
+ // loop over selector entities
+ selector.elements.forEach(function (element) {
+
+ var extract
+
+ // continue to next element if no js- prefix
+ if (!RULE.exp.test(element.value)) return
+
+ // calculate line number for the extract
+ extract = util.getLine(element.index - element.value.length, data)
+ extract = util.padLine(extract)
+
+ // highlight invalid styling of ID
+ extract += element.value.replace(RULE.exp, '#'.magenta)
+
+ // set invalid flag to false
+ isValid = false
+
+ // set error object on defintion token
+ util.throwError(def, {
+ type: RULE.type
+ , message: RULE.message
+ , extract: extract
+ })
+
+ })
+ })
+
+ // return valid state
+ return isValid
+}
62 lib/lint/no-JS-prefix.js
@@ -0,0 +1,62 @@
+// ==========================================
+// RECESS
+// RULE: .js prefixes should not be styled
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under the Apache License v2.0
+// http://www.apache.org/licenses/LICENSE-2.0
+// ==========================================
+
+'use strict'
+
+var util = require('../util')
+ , RULE = {
+ type: 'noJSPrefix'
+ , exp: /^\.js\-/
+ , message: '.js prefixes should not be styled'
+ }
+
+// validation method
+module.exports = function (def, data) {
+
+ // default validation to true
+ var isValid = true
+
+ // return if no selector to validate
+ if (!def.selectors) return isValid
+
+ // loop over selectors
+ def.selectors.forEach(function (selector) {
+
+ // loop over selector entities
+ selector.elements.forEach(function (element) {
+
+ var extract
+
+ // continue to next element if .js- prefix not styled
+ if (!RULE.exp.test(element.value)) return
+
+ // calculate line number for the extract
+ extract = util.getLine(element.index - element.value.length, data)
+ extract = util.padLine(extract)
+
+ // highlight invalid styling of .js- prefix
+ extract += element.value.replace(RULE.exp, '.js-'.magenta)
+
+ // set invalid flag to false
+ isValid = false
+
+ // set error object on defintion token
+ util.throwError(def, {
+ type: RULE.type
+ , message: RULE.message
+ , extract: extract
+ })
+
+ })
+
+ })
+
+ // return valid state
+ return isValid
+}
59 lib/lint/no-overqualifying.js
@@ -0,0 +1,59 @@
+// ==========================================================
+// RECESS
+// RULE: Underscores should not be used when naming selectors
+// ==========================================================
+// Copyright 2012 Twitter, Inc
+// Licensed under the Apache License v2.0
+// http://www.apache.org/licenses/LICENSE-2.0
+// ==========================================================
+
+'use strict'
+
+var util = require('../util')
+ , RULE = {
+ type: 'noOverqualifying'
+ , exp: /\b[\w\-\_]+(?=#|\.)/
+ , message: 'Element selectors should not be overqualified'
+ }
+
+// validation method
+module.exports = function (def, data) {
+
+ // default validation to true
+ var isValid = true
+
+ // return if no selector to validate
+ if (!def.selectors) return isValid
+
+ // loop over selectors
+ def.selectors.forEach(function (selector) {
+
+ // evaluate selector to string and trim whitespace
+ var selectorString = selector.toCSS().trim()
+ , extract
+
+ // if selector isn't overqualified continue
+ if (!RULE.exp.test(selectorString)) return
+
+ // calculate line number for the extract
+ extract = util.getLine(selector.elements[0].index - selector.elements[0].value.length, data)
+ extract = util.padLine(extract)
+
+ // highlight selector overqualification
+ extract += selectorString.replace(RULE.exp, function ($1) { return $1.magenta })
+
+ // set invalid flag to false
+ isValid = false
+
+ // set error object on defintion token
+ util.throwError(def, {
+ type: RULE.type
+ , message: RULE.message
+ , extract: extract
+ })
+
+ })
+
+ // return validation state
+ return isValid
+}
62 lib/lint/no-underscores.js
@@ -0,0 +1,62 @@
+// ==========================================================
+// RECESS
+// RULE: Underscores should not be used when naming selectors
+// ==========================================================
+// Copyright 2012 Twitter, Inc
+// Licensed under the Apache License v2.0
+// http://www.apache.org/licenses/LICENSE-2.0
+// ==========================================================
+
+'use strict'
+
+var util = require('../util')
+ , RULE = {
+ type: 'noUnderscores'
+ , exp: /_/g
+ , message: 'Underscores should not be used when naming selectors'
+ }
+
+// validation method
+module.exports = function validate(def, data) {
+
+ // default validation to true
+ var isValid = true
+
+ // return if no selector to validate
+ if (!def.selectors) return isValid
+
+ // loop over selectors
+ def.selectors.forEach(function (selector) {
+
+ // loop over selector entities
+ selector.elements.forEach(function (element) {
+
+ var extract
+
+ // continue to next element if no underscore
+ if (!RULE.exp.test(element.value)) return
+
+ // calculate line number for the extract
+ extract = util.getLine(element.index - element.value.length, data)
+ extract = util.padLine(extract)
+
+ // highlight invalid underscores
+ extract += element.value.replace(RULE.exp, '_'.magenta)
+
+ // set invalid flag to false
+ isValid = false
+
+ // set error object on defintion token
+ util.throwError(def, {
+ type: RULE.type
+ , message: RULE.message
+ , extract: extract
+ })
+
+ })
+ })
+
+ // return valid state
+ return isValid
+
+}
62 lib/lint/no-universal-selectors.js
@@ -0,0 +1,62 @@
+// ===========================================
+// RECESS
+// RULE: Universal selectors should be avoided
+// ===========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under the Apache License v2.0
+// http://www.apache.org/licenses/LICENSE-2.0
+// ===========================================
+
+'use strict'
+
+var util = require('../util')
+ , RULE = {
+ type: 'noUniversalSelectors'
+ , exp: /\*/g
+ , message: 'Universal selectors should be avoided'
+ }
+
+// validation method
+module.exports = function (def, data) {
+
+ // default validation to true
+ var isValid = true
+
+ // return if no rules to validate
+ if (!def.selectors) return isValid
+
+ // loop over selectors
+ def.selectors.forEach(function (selector) {
+
+ // loop over selector entities
+ selector.elements.forEach(function (element) {
+
+ var extract
+
+ // continue to next element if no underscore
+ if (!RULE.exp.test(element.value)) return
+
+ // calculate line number for the extract
+ extract = util.getLine(element.index - element.value.length, data)
+ extract = util.padLine(extract)
+
+ // highlight the invalid use of a universal selector
+ extract += selector.toCSS({}).replace(RULE.exp, '*'.magenta)
+
+ // set invalid flag to false
+ isValid = false
+
+ // set error object on defintion token
+ util.throwError(def, {
+ type: RULE.type
+ , message: RULE.message
+ , extract: extract
+ })
+
+ })
+ })
+
+ // return valid state
+ return isValid
+
+}
269 lib/lint/strict-property-order.js
@@ -0,0 +1,269 @@
+// ==========================================
+// RECESS
+// RULE: Must use correct property ordering
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under the Apache License v2.0
+// http://www.apache.org/licenses/LICENSE-2.0
+// ==========================================
+
+'use strict'
+
+var _ = require('underscore')
+ , util = require('../util')
+ , RULE = {
+ type: 'strictPropertyOrder'
+ , message: 'Incorrect property order for rule'
+ }
+
+ // vendor prefix order
+ , vendorPrefixes = [
+ '-webkit-'
+ , '-khtml-'
+ , '-epub-'
+ , '-moz-'
+ , '-ms-'
+ , '-o-'
+ ]
+
+ // hack prefix order
+ , hackPrefixes = [
+ '_' // ie7
+ , '*' // ie6
+ ]
+
+ // css property order
+ , order = [
+ 'position'
+ , 'top'
+ , 'right'
+ , 'bottom'
+ , 'left'
+ , 'z-index'
+ , 'display'
+ , 'float'
+ , 'width'
+ , 'height'
+ , 'max-width'
+ , 'max-height'
+ , 'min-width'
+ , 'min-height'
+ , 'padding'
+ , 'padding-top'
+ , 'padding-right'
+ , 'padding-bottom'
+ , 'padding-left'
+ , 'margin'
+ , 'margin-top'
+ , 'margin-right'
+ , 'margin-bottom'
+ , 'margin-left'
+ , 'margin-collapse'
+ , 'margin-top-collapse'
+ , 'margin-right-collapse'
+ , 'margin-bottom-collapse'
+ , 'margin-left-collapse'
+ , 'overflow'
+ , 'overflow-x'
+ , 'overflow-y'
+ , 'clip'
+ , 'clear'
+ , 'font'
+ , 'font-family'
+ , 'font-size'
+ , 'font-smoothing'
+ , 'font-style'
+ , 'font-weight'
+ , 'src'
+ , 'line-height'
+ , 'letter-spacing'
+ , 'word-spacing'
+ , 'color'
+ , 'text-align'
+ , 'text-decoration'
+ , 'text-indent'
+ , 'text-overflow'
+ , 'text-rendering'
+ , 'text-size-adjust'
+ , 'text-shadow'
+ , 'text-transform'
+ , 'word-break'
+ , 'word-wrap'
+ , 'white-space'
+ , 'vertical-align'
+ , 'list-style'
+ , 'cursor'
+ , 'background'
+ , 'background-attachment'
+ , 'background-color'
+ , 'background-image'
+ , 'background-position'
+ , 'background-repeat'
+ , 'background-size'
+ , 'border'
+ , 'border-collapse'
+ , 'border-top'
+ , 'border-right'
+ , 'border-bottom'
+ , 'border-left'
+ , 'border-color'
+ , 'border-top-color'
+ , 'border-right-color'
+ , 'border-bottom-color'
+ , 'border-left-color'
+ , 'border-spacing'
+ , 'border-style'
+ , 'border-top-style'
+ , 'border-right-style'
+ , 'border-bottom-style'
+ , 'border-left-style'
+ , 'border-width'
+ , 'border-top-width'
+ , 'border-right-width'
+ , 'border-bottom-width'
+ , 'border-left-width'
+ , 'border-radius'
+ , 'border-top-right-radius'
+ , 'border-bottom-right-radius'
+ , 'border-bottom-left-radius'
+ , 'border-top-left-radius'
+ , 'border-radius-topright'
+ , 'border-radius-bottomright'
+ , 'border-radius-bottomleft'
+ , 'border-radius-topleft'
+ , 'content'
+ , 'quotes'
+ , 'outline'
+ , 'outline-offset'
+ , 'opacity'
+ , 'filter'
+ , 'visibility'
+ , 'size'
+ , 'zoom'
+ , 'box-shadow'
+ , 'animation'
+ , 'animation-delay'
+ , 'animation-duration'
+ , 'animation-iteration-count'
+ , 'animation-name'
+ , 'animation-play-state'
+ , 'animation-timing-function'
+ , 'transition'
+ , 'transition-delay'
+ , 'transition-duration'
+ , 'transition-property'
+ , 'transition-timing-function'
+ , 'background-clip'
+ , 'box-sizing'
+ , 'resize'
+ , 'appearance'
+ , 'user-select'
+ , 'interpolation-mode'
+ , 'direction'
+ , 'marks'
+ , 'page'
+ , 'set-link-source'
+ , 'unicode-bidi'
+ ]
+
+ // regex tests
+ , HACK_PREFIX = new RegExp('^(' + hackPrefixes.join('|').replace(/[-[\]{}()*+?.,\\^$#\s]/g, "\\$&") + ')')
+ , VENDOR_PREFIX = new RegExp('^(' + vendorPrefixes.join('|').replace(/[-[\]{}()*+?.,\\^$#\s]/g, "\\$&") + ')')
+
+
+// validation method
+module.exports = function (def, data) {
+
+ // // default validation to true
+ var isValid = true
+ , dict = {}
+ , index = 0
+ , cleanRules
+ , sortedRules
+ , firstLine
+ , extract
+ , selector
+
+ // return if no rules to validate
+ if (!def.rules) return isValid
+
+ // recurse over nested rulesets
+ def.rules.forEach(function (rule) {
+ if (rule.selectors) module.exports(rule, data)
+ })
+
+ cleanRules = def.rules.map(function (rule) {
+ return rule.name && rule
+ }).filter(function (item) { return item })
+
+ // sort rules
+ sortedRules = _.sortBy(cleanRules, function (rule) {
+
+ // pad value of each rule position to account for vendor prefixes
+ var padding = (vendorPrefixes.length + 1) * 10
+ , root
+ , val
+
+ // strip vendor prefix and hack prefix from rule name to find root
+ root = rule.name
+ .replace(VENDOR_PREFIX, '')
+ .replace(HACK_PREFIX, '')
+
+ // find value of order of the root css property
+ val = order.indexOf(root)
+
+ // if property is not found, exit with property not found error
+ if (!~val) {
+ return util.throwError(def, {
+ type: 'propertyNotFound'
+ , message: 'Unknown property name: "' + rule.name + '"'
+ })
+ }
+
+ // pad value
+ val = (val * padding) + 10
+
+ // adjust value based on prefix
+ val += VENDOR_PREFIX.exec(rule.name) ? vendorPrefixes.indexOf(RegExp.$1) : (vendorPrefixes.length + 1)
+
+ // adjust value based on css hack
+ val += HACK_PREFIX.exec(rule.name) ? (hackPrefixes.indexOf(RegExp.$1)) : 0
+
+ // return sort value
+ return val
+ })
+
+ // check to see if sortedRules has same order as provided rules
+ isValid = _.isEqual(sortedRules, cleanRules)
+
+ // return if sort is correct
+ if (isValid) return isValid
+
+ // get the line number of the first rule
+ firstLine = util.getLine(def.rules[0].index, data)
+
+ // generate a extract what the correct sorted rules would look like
+ extract = sortedRules.map(function (rule) {
+ if (!rule.name) return
+ return util.padLine(firstLine + index++)
+ + ' ' + rule.name + ': '
+ + (typeof rule.value == 'string' ? rule.value : rule.value.toCSS({}))
+ + ';'
+ }).filter(function (item) { return item }).join('\n')
+
+ // extract selector for error message
+ selector = (' "' + def.selectors.map(function (selector) {
+ return selector.toCSS && selector.toCSS({}).replace(/^\s/, '')
+ }).join(', ') + '"').magenta
+
+ // set error object on defintion token
+ util.throwError(def, {
+ type: RULE.type
+ , message: RULE.message + selector + '\n\n Correct order below:\n'.grey
+ , extract: extract
+ , sortedRules: sortedRules
+ })
+
+ // return valid state
+ return isValid
+}
71 lib/lint/zero-units.js
@@ -0,0 +1,71 @@
+// ================================================
+// RECESS
+// RULE: No need to specify units when a value is 0
+// ================================================
+// Copyright 2012 Twitter, Inc
+// Licensed under the Apache License v2.0
+// http://www.apache.org/licenses/LICENSE-2.0
+// ================================================
+
+'use strict'
+
+var util = require('../util')
+ , units = [
+ '%'
+ , 'in'
+ , 'cm'
+ , 'mm'
+ , 'em'
+ , 'ex'
+ , 'pt'
+ , 'pc'
+ , 'px'
+ ]
+ , RULE = {
+ type: 'zeroUnits'
+ , exp: new RegExp('\\b0\\s?(' + units.join('|') + ')')
+ , message: 'No need to specify units when a value is 0'
+ }
+
+// validation method
+module.exports = function (def, data) {
+
+ // default validation to true
+ var isValid = true
+
+ // return if no rules to validate
+ if (!def.rules) return isValid
+
+ // loop over rules
+ def.rules.forEach(function (rule) {
+ var extract
+
+ // continue to next rule if no 0 units are present
+ if ( !(rule.value
+ && rule.value.is == 'value'
+ && RULE.exp.test(rule.value.toCSS({}))) ) return
+
+ // calculate line number for the extract
+ extract = util.getLine(rule.index, data)
+ extract = util.padLine(extract)
+
+ // highlight invalid 0 units
+ extract += rule.toCSS({}).replace(RULE.exp, function ($1) {
+ return 0 + $1.slice(1).magenta
+ })
+
+ // set invalid flag to false
+ isValid = false
+
+ // set error object on defintion token
+ util.throwError(def, {
+ type: RULE.type
+ , message: RULE.message
+ , extract: extract
+ })
+
+ })
+
+ // return valid state
+ return isValid
+}
47 lib/util.js
@@ -0,0 +1,47 @@
+// ==========================================
+// RECESS
+// UTIL: simple output util methods
+// ==========================================
+// Copyright 2012 Twitter, Inc
+// Licensed under the Apache License v2.0
+// http://www.apache.org/licenses/LICENSE-2.0
+// ==========================================
+
+'use strict'
+
+var _ = require('underscore')
+
+module.exports = {
+
+ // set fail output object
+ throwError: function (def, err) {
+ def.errors = def.errors || []
+ err.message = err.message.cyan
+ def.errors.push(err)
+ }
+
+ // set line padding
+, padLine: function (line) {
+ var num = (line + '. ')
+ , space = ''
+ _.times(10 - num.length, function () { space += ' ' })
+ return (space + num).grey
+ }
+
+ // get line number from data
+, getLine: function (index, data) {
+ return (data.slice(0, index).match(/\n/g) || "").length + 1;
+ }
+
+ // error counter
+, countErrors: function (definitions) {
+ var fails = 0
+ definitions.forEach(function (def) {
+ def.errors
+ && def.errors.length
+ && def.errors.forEach(function (err) { fails++ })
+ })
+ return fails
+ }
+
+}
14 makefile
@@ -0,0 +1,14 @@
+#
+# Run all tests
+#
+test:
+ @@ node test
+
+#
+# Run benchmark
+#
+benchmark:
+ @@ node benchmark
+
+
+.PHONY: test benchmark
14 package.json
@@ -0,0 +1,14 @@
+{ "name": "recess"
+, "description": "A simple, attractive code quality tool for CSS built on top of LESS"
+, "version": "0.1.0"
+, "author": "Jacob Thornton <jacob@twitter.com> (https://github.com/fat)"
+, "keywords": ["css", "lint"]
+, "licenses": [ { "type": "Apache-2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0" } ]
+, "main": "./lib"
+, "homepage": "http://twitter.github.com/recess"
+, "engines": { "node": ">= 0.4.0" }
+, "dependencies": { "colors": ">= 0.3.0", "nopt": ">= 1.0.10", "underscore": ">= 1.2.1", "less": ">= 1.3.0" }
+, "directories": { "bin": "./bin" }
+, "scripts": { "test": "node test" }
+, "bin": { "recess": "./bin/recess" }
+, "preferGlobal": true }
139 test/compiled/blog.css
@@ -0,0 +1,139 @@
+/* Fat's blog styles */
+
+@font-face {
+ font-family: "Mistral";
+ src: url("/fonts/Mistral.ttf");
+}
+
+html,
+body {
+ overflow: auto;
+}
+
+h1 a {
+ margin: 0;
+ font-family: "Mistral", helvetica;
+ font-size: 48px;
+ color: #f600ff;
+}
+
+h1 a:hover {
+ color: #f600ff;
+ text-decoration: none;
+}
+
+body > article,
+body > header,
+body > section,
+body > footer {
+ width: 525px;
+ padding: 0 50px;
+}
+
+header h1 {
+ margin-bottom: 0;
+}
+
+body > header {
+ padding-top: 50px;
+}
+
+body > footer {
+ padding-bottom: 20px;
+}
+
+body > footer,
+body > section {
+ overflow: hidden;
+}
+
+article header {
+ margin-bottom: 15px;
+}
+
+footer {
+ margin-top: 0;
+ border: 0;
+}
+
+a,
+a:hover {
+ color: #000;
+}
+
+p a {
+ text-decoration: underline;
+}
+
+p {
+ color: #555;
+}
+
+.post {
+ margin: 30px 0;
+}
+
+section .post img {
+ float: left;
+ width: 250px;
+ margin-right: 25px;
+ margin-bottom: 0;
+}
+
+.post img {
+ width: 500px;
+ margin-bottom: -25px;
+}
+
+.post::after {
+ display: block;
+ width: auto;
+ height: 8px;
+ margin: 0 auto;
+ margin-top: 31px;
+ background-color: #000;
+ content: '';
+}
+
+.avatar {
+ position: relative;
+ float: left;
+ margin-right: 25px;
+}
+
+.avatar img {
+ position: relative;
+ display: block;
+ width: 189px;
+ margin: 0 auto;
+ border-radius: 10px;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
+}
+
+.avatar img::after {
+ position: absolute;
+ top: 0;
+ left: 0;
+ display: block;
+ width: 32px;
+ height: 32px;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ content: " ";
+ -webkit-box-shadow: inset 0 1px 5px rgba(0, 0, 0, 0.2);
+ -moz-box-shadow: inset 0 1px 5px rgba(0, 0, 0, 0.2);
+ -ms-box-shadow: inset 0 1px 5px rgba(0, 0, 0, 0.2);
+ -o-box-shadow: inset 0 1px 5px rgba(0, 0, 0, 0.2);
+ box-shadow: inset 0 1px 5px rgba(0, 0, 0, 0.2);
+}
+
+article p {
+ margin-bottom: 20px;
+ font-size: 14px;
+ line-height: 25px;
+}
+
+pre code {
+ background: transparent;
+}
16 test/compiled/no-IDs.css
@@ -0,0 +1,16 @@
+div,
+#div:hover {
+ position: absolute;
+}
+
+p,
+.cool,
+#bar {
+ display: block;
+}
+
+p,
+.cool,
+.foo > #bar {
+ display: block;
+}
21 test/compiled/no-JS.css
@@ -0,0 +1,21 @@
+html,
+body,
+.js-one,
+.js-two,
+h4,
+h5 {
+ display: block;
+}
+
+html,
+body,
+.js-three,
+.js-four {
+ display: block;
+}
+
+.js-five,
+h1,
+.js-six {
+ display: block;
+}
10 test/compiled/no-overqualifying.css
@@ -0,0 +1,10 @@
+div.fat {
+ position: absolute;
+ display: block;
+}
+
+h1,
+.foo h2#ded,
+h3.mdo {
+ overflow: hidden;
+}
9 test/compiled/no-underscores.css
@@ -0,0 +1,9 @@
+.foo_bar_baz {
+ display: block;
+}
+
+html,
+div.i_am_fat + bar,
+body {
+ display: block;
+}
252 test/compiled/preboot.css
@@ -0,0 +1,252 @@
+/*
+ Bootstrap v1.1
+ Variables and mixins to bootstrap any new web development project.
+*/
+
+/* Variables
+-------------------------------------------------- */
+
+/* Mixins
+-------------------------------------------------- */
+
+.clearfix {
+ zoom: 1;
+}
+
+.clearfix:after {
+ display: block;
+ height: 0;
+ clear: both;
+ content: ".";
+ visibility: hidden;
+}
+
+.center-block {
+ display: block;
+ margin: 0 auto;
+}
+
+.container {
+ width: 940px;
+ margin: 0 auto;
+ zoom: 1;
+}
+
+.container:after {
+ display: block;
+ height: 0;
+ clear: both;
+ content: ".";
+ visibility: hidden;
+}
+
+#flexbox .display-box {
+ display: -moz-box;
+ display: -webkit-box;
+ display: box;
+}
+
+#reset .global-reset html,
+#reset .global-reset body,
+#reset .global-reset div,
+#reset .global-reset span,
+#reset .global-reset applet,
+#reset .global-reset object,
+#reset .global-reset iframe,
+#reset .global-reset h1,
+#reset .global-reset h2,
+#reset .global-reset h3,
+#reset .global-reset h4,
+#reset .global-reset h5,
+#reset .global-reset h6,
+#reset .global-reset p,
+#reset .global-reset blockquote,
+#reset .global-reset pre,
+#reset .global-reset a,
+#reset .global-reset abbr,
+#reset .global-reset acronym,
+#reset .global-reset address,
+#reset .global-reset big,
+#reset .global-reset cite,
+#reset .global-reset code,
+#reset .global-reset del,
+#reset .global-reset dfn,
+#reset .global-reset em,
+#reset .global-reset img,
+#reset .global-reset ins,
+#reset .global-reset kbd,
+#reset .global-reset q,
+#reset .global-reset s,
+#reset .global-reset samp,
+#reset .global-reset small,
+#reset .global-reset strike,
+#reset .global-reset strong,
+#reset .global-reset sub,
+#reset .global-reset sup,
+#reset .global-reset tt,
+#reset .global-reset var,
+#reset .global-reset b,
+#reset .global-reset u,
+#reset .global-reset i,
+#reset .global-reset center,
+#reset .global-reset dl,
+#reset .global-reset dt,
+#reset .global-reset dd,
+#reset .global-reset ol,
+#reset .global-reset ul,
+#reset .global-reset li,
+#reset .global-reset fieldset,
+#reset .global-reset form,
+#reset .global-reset label,
+#reset .global-reset legend,
+#reset .global-reset table,
+#reset .global-reset caption,
+#reset .global-reset tbody,
+#reset .global-reset tfoot,
+#reset .global-reset thead,
+#reset .global-reset tr,
+#reset .global-reset th,
+#reset .global-reset td,
+#reset .global-reset article,
+#reset .global-reset aside,
+#reset .global-reset canvas,
+#reset .global-reset details,
+#reset .global-reset embed,
+#reset .global-reset figure,
+#reset .global-reset figcaption,
+#reset .global-reset footer,
+#reset .global-reset header,
+#reset .global-reset hgroup,
+#reset .global-reset menu,
+#reset .global-reset nav,
+#reset .global-reset output,
+#reset .global-reset ruby,
+#reset .global-reset section,
+#reset .global-reset summary,
+#reset .global-reset time,
+#reset .global-reset mark,
+#reset .global-reset audio,
+#reset .global-reset video {
+ padding: 0;
+ margin: 0;
+ font: inherit;
+ font-size: 100%;
+ vertical-align: baseline;
+ border: 0;
+}
+
+#reset .global-reset body {
+ line-height: 1;
+}
+
+#reset .global-reset ol,
+#reset .global-reset ul {
+ list-style: none;
+}
+
+#reset .global-reset table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+#reset .global-reset caption,
+#reset .global-reset th,
+#reset .global-reset td {
+ font-weight: normal;
+ text-align: left;
+ vertical-align: middle;
+}
+
+#reset .global-reset q,
+#reset .global-reset blockquote {
+ quotes: none;
+}
+
+#reset .global-reset q:before,
+#reset .global-reset blockquote:before,
+#reset .global-reset q:after,
+#reset .global-reset blockquote:after {
+ content: "";
+ content: none;
+}
+
+#reset .global-reset a img {
+ border: none;
+}
+
+#reset .global-reset article,
+#reset .global-reset aside,
+#reset .global-reset details,
+#reset .global-reset figcaption,
+#reset .global-reset figure,
+#reset .global-reset footer,
+#reset .global-reset header,
+#reset .global-reset hgroup,
+#reset .global-reset menu,
+#reset .global-reset nav,
+#reset .global-reset section {
+ display: block;
+}
+
+#reset .reset-box-model {
+ padding: 0;
+ margin: 0;
+ border: 0;
+}
+
+#reset .reset-font {
+ font: inherit;
+ font-size: 100%;
+ vertical-align: baseline;
+}
+
+#reset .reset-focus {
+ outline: 0;
+}
+
+#reset .reset-body {
+ line-height: 1;
+}
+
+#reset .reset-list-style {
+ list-style: none;
+}
+
+#reset .reset-table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+#reset .reset-table-cell {
+ font-weight: normal;
+ text-align: left;
+ vertical-align: middle;
+}
+
+#reset .reset-quotation {
+ quotes: none;
+}
+
+#reset .reset-quotation:before,
+#reset .reset-quotation:after {
+ content: "";
+ content: none;
+}
+
+#reset .reset-image-anchor-border {
+ border: none;
+}
+
+#reset .reset-html5 article,
+#reset .reset-html5 aside,
+#reset .reset-html5 details,
+#reset .reset-html5 figcaption,
+#reset .reset-html5 figure,
+#reset .reset-html5 footer,
+#reset .reset-html5 header,
+#reset .reset-html5 hgroup,
+#reset .reset-html5 menu,
+#reset .reset-html5 nav,
+#reset .reset-html5 section {
+ display: block;
+}
7 test/compiled/prefixes.css
@@ -0,0 +1,7 @@
+div {
+ -webkit-border-radius: 2px;
+ -moz-border-radius: 2px;
+ -ms-border-radius: 2px;
+ -o-border-radius: 2px;
+ border-radius: 2px;
+}
11 test/compiled/property-order.css
@@ -0,0 +1,11 @@
+html,
+body,
+h1,
+h3:hover {
+ position: absolute;
+ display: block;
+ font: arial 12px/2px bold;
+ font-size: 2px;
+ color: red;
+ background: nutmeg;
+}
4 test/compiled/simple.css
@@ -0,0 +1,4 @@
+body {
+ position: absolute;
+ display: block;
+}
13 test/compiled/universal-selectors.css
@@ -0,0 +1,13 @@
+* {
+ display: block;
+}
+
+.ded > *,
+.mdo > *,
+.fat > * {
+ position: absolute;
+}
+
+.fat * {
+ overflow: hidden;
+}
16 test/compiled/zero-units.css
@@ -0,0 +1,16 @@
+div {
+ position: absolute;
+ margin: 0;
+ margin: 0;
+ margin: 0;
+ margin: 0;
+ margin: 0 auto;
+ margin: 0 2px 3px;
+ color: rgba(0, 0, 0, 0.3);
+ color: #0ebcdc;
+}
+
+h1,
+h2 {
+ font: Arial 0;
+}
139 test/fixtures/blog.css
@@ -0,0 +1,139 @@
+/* Fat's blog styles */
+
+@font-face {
+ font-family: "Mistral";
+ src: url("/fonts/Mistral.ttf");
+}
+
+html,
+body {
+ overflow: auto;
+}
+
+h1 a {
+ margin: 0px;
+ font-family: "Mistral", helvetica;
+ font-size: 48px;
+ color: #f600ff;
+}
+
+h1 a:hover {
+ color: #f600ff;
+ text-decoration: none;
+}
+
+body > article,
+body > header,
+body > section,
+body > footer {
+ width: 525px;
+ padding: 0 50px;
+}
+
+header h1 {
+ margin-bottom: 0;
+}
+
+body > header {
+ padding-top: 50px;
+}
+
+body > footer {
+ padding-bottom: 20px;
+}
+
+body > footer,
+body > section {
+ overflow: hidden;
+}