Skip to content
Browse files

Added input filtering

  • Loading branch information...
1 parent 8425737 commit 2aef35ae99a5b72effc8e6c1a7892d2002f57d74 @Qard committed Feb 16, 2012
Showing with 268 additions and 82 deletions.
  1. +44 −1 README.md
  2. +222 −79 lib/index.js
  3. +2 −2 package.json
View
45 README.md
@@ -65,6 +65,37 @@ or
var url = crsh_libs.getUrl()
+### NEW! Filters
+
+ // Let's try a csv-to-json filter
+ Crsh.addFilter('csv', function () {
+ var csv = require('csv')
+ return function () {
+ this.addType('json', 'csv')
+
+ return function (data, next) {
+ var pattern = /(?:^|,)("(?:[^"]+)*"|[^,]*)/g
+ , lines = data.split("\n")
+ , keys = lines.shift()
+ .split(pattern)
+ .map(function (key) {
+ return key.toLowerCase()
+ })
+ , rows = lines.map(function (line) {
+ var res = {}
+ line.split(pattern).forEach(function (val, i) {
+ if (keys[i]) {
+ res[keys[i]] = val.replace(/"/g, '')
+ }
+ })
+ return res
+ })
+
+ callback(null, JSON.stringify(rows))
+ }
+ }
+ })
+
## API
### new Crsh([basePath], [filePaths])
@@ -73,6 +104,18 @@ Constructs a new block, compiles and starts the watcher, if watcher is allowed i
### crsh.add|remove(path)
Add or remove a file by its path from the block and watcher. Note that this will not recompile the block. In this particular case crsh.compile() should be called manually.
+### crsh.addType(outputType, ext)
+Tells crsh to allow files of the specified extension and that they represent files of outputType. For example; crsh.addType('js', 'coffee') would tell it that Coffeescript files are allowed and should understood as Javascript. You'll still need to supply a filter to actually convert the Coffeescript though.
+
+### crsh.addFilter(ext, filter)
+Add a filter function to manipulate each file of the given file extension. Filters can be in two formats; a function which receives (data, next) or a closure which returns a (data, next) receiver. The closure type is handy for applying some modifications to Crsh beforehand. For example; using addType() to allow the file type we are filtering.
+
+### crsh.findFilter(ext)
+Retrieve the currently assigned filter for the supplied extension. This is meant to be used internally during the compile phase.
+
+### crsh.coffee|stylus|less()
+Crsh comes with some pre-made filters. For backwards compatibility, coffee and stylus filters are added automatically. These filters will need to be added explicitly in the next version though, be warned. Use them like this; crsh.addFilter('less', crsh.less())
+
### crsh.compile(callback)
Forces the current block to compile. This is called automatically when a new block is constructed with a file_paths list or the watcher is enable and a change has occured. When chaining, this is called manually instead.
@@ -83,7 +126,7 @@ Gets a handy url fragment containing the name of the block file and a modificati
For each item in blockDefs, constructs a new Crsh instance and exposes it in req.crsh[name]. Also creates a "crsh_{name}" properties in view locals.
### Crsh.getFormat(filename_or_path)
-Will return "js" if the extension is .js or .coffee, will return .css if the extension is .css or .styl and will otherwise return false.
+Determines file extension and returns the outputType, if known, or false.
### Crsh.isJs|isCss(filename_or_path)
Convenient aliases to Crsh.getFormat to determine if the format is what you expect.
View
301 lib/index.js
@@ -2,11 +2,13 @@ var helper = require('./helper')
, Files = require('./files')
, Watch = require('./watcher')
, uglify = require('uglify-js')
+ , async = require('async')
, path = require('path')
, jsp = uglify.parser
, pro = uglify.uglify
, coffee = null
, stylus = null
+ , isProd = process.env.NODE_ENV === 'production'
/**
* Construct a Crsh block
@@ -27,7 +29,7 @@ function Crsh (path, list) {
this.path = path
// Don't use watcher in production
- if (process.env.NODE_ENV !== 'production') {
+ if ( ! isProd) {
this.watcher = new Watch(path)
}
@@ -44,7 +46,9 @@ function Crsh (path, list) {
// Run the compiler when a change occurs
if (this.watcher) {
this.watcher.on('change', function () {
- self.compile()
+ self.compile(function (err) {
+ if (err) { throw new Error(err) }
+ })
})
}
}
@@ -55,14 +59,159 @@ Crsh.path = process.cwd()
/**
+ * Filters
+ */
+Crsh.prototype.filters = {}
+
+
+/**
+ * Add a file type filter
+ *
+ * @param string
+ * File extension to identify type by
+ *
+ * @param function
+ * Filter construction closure
+ */
+Crsh.prototype.addFilter = function (type, fn) {
+ type = noDot(type)
+ this.filters[type] = fn.length ? fn : fn.call(this)
+ return this
+}
+
+
+/**
+ * Find an existing file type filter
+ *
+ * @param string
+ * The extension to search with
+ */
+Crsh.prototype.findFilter = function (type) {
+ type = noDot(type)
+ return this.filters[type] || function (file, next) {
+ next(null, file)
+ }
+}
+
+
+/**
+ * Filter presets
+ */
+Crsh.coffee = function () {
+ try {
+ var coffee = require('coffee-script')
+ } catch (e) {
+ throw new Error('coffee-script not installed')
+ }
+
+ return function () {
+ this.addType('js', 'coffee')
+
+ return function (file, next) {
+ process.nextTick(function () {
+ try {
+ var res = coffee.compile(file)
+ next(null, res)
+ } catch (e) {
+ next(e)
+ }
+ })
+ }
+ }
+}
+
+Crsh.stylus = function () {
+ try {
+ var stylus = require('stylus')
+ } catch (e) {
+ throw new Error('stylus not installed')
+ }
+
+ return function () {
+ this.addType('css', 'styl')
+
+ return function (file, next) {
+ stylus(file)
+ .set('compress', isProd)
+ .set('filename', 'style.styl')
+ .render(next)
+ }
+ }
+}
+
+Crsh.less = function (conf) {
+ try {
+ var less = require('less')
+ } catch (e) {
+ throw new Error('less not installed')
+ }
+
+ return function () {
+ this.addType('css', 'less')
+
+ if ( ! conf) {
+ conf = { paths: [this.path] }
+ }
+
+ var parser = new (less.Parser)(conf)
+
+ return function (file, next) {
+ parser.parse(file, function (err, tree) {
+ try {
+ next(err, err ? null : tree.toCSS({
+ compress: !!conf.compress
+ }))
+ } catch (e) {
+ // Y U NO THROW REAL ERRORS, LESS??
+ next(new Error(e.message))
+ }
+ })
+ }
+ }
+}
+
+
+/**
+ * Supported output types and input types that map to them
+ */
+Crsh.prototype.types = { 'js': ['js'], 'css': ['css'] }
+
+function noDot (t) {
+ return t[0] === '.' ? t.substr(1) : t
+}
+
+/**
+ * Add a type to supported types.
+ * It's recommended that added filters
+ * call this in the registration process.
+ *
+ * @param string
+ * The output type it should represent
+ *
+ * @param string
+ * File extension to match against
+ */
+Crsh.prototype.addType = function (type, ext) {
+ type = noDot(type)
+ ext = noDot(ext)
+ if (typeof this.types[type] === 'undefined') {
+ this.types[type] = []
+ }
+ if (this.types[type].indexOf(ext) < 0) {
+ this.types[type].push(ext)
+ }
+}
+
+
+/**
* Add a file to the watcher
*
* @param string
* Relative or absolute path to file
*/
Crsh.prototype.add = function (f) {
f = helper.realPath(f, this.path)
- var format = Crsh.getFormat(f)
+ var format = this.getFormat(f)
// Mixing formats is bad
if (this.format && this.format !== format) {
@@ -72,9 +221,6 @@ Crsh.prototype.add = function (f) {
// Make sure format is set
this.format || (this.format = format)
- // Ensure necessary parsers are available
- this.prepParsers(f)
-
// Add to list
this.list.push(f)
@@ -85,49 +231,24 @@ Crsh.prototype.add = function (f) {
}
/**
- * Determines if a parser library is need and loads it
+ * Attempt to identify file type
*
* @param string
* File name or path
*/
-Crsh.prototype.prepParsers = function (file) {
- var ext = path.extname(file)
-
- // Needs coffeescript
- if ('.coffee' === ext) {
- try {
- coffee = require('coffee-script')
- } catch (e) {
- throw new Error('coffee-script not installed')
- }
- }
-
- // Needs stylus
- if ('.styl' === ext) {
- try {
- stylus = require('stylus')
- } catch (e) {
- throw new Error('stylus not installed')
+Crsh.prototype.getFormat = function (file) {
+ var ext = noDot(path.extname(file))
+ for (var i in this.types) {
+ if (this.types[i].indexOf(ext) >= 0) {
+ return i
}
}
-}
-
-/**
- * Attempt to identify file type
- *
- * @param string
- * File name or path
- */
-Crsh.getFormat = function (file) {
- var ext = path.extname(file)
- return ['.js','.coffee'].indexOf(ext) >= 0 ? 'js'
- : ['.css','.styl'].indexOf(ext) >= 0 ? 'css'
- : false
+ return false
}
// Aliases for getFormat
-Crsh.isJs = function (f) { return 'js' === Crsh.getFormat(f) }
-Crsh.isCss = function (f) { return 'css' === Crsh.getFormat(f) }
+Crsh.prototype.isJs = function (f) { return 'js' === this.getFormat(f) }
+Crsh.prototype.isCss = function (f) { return 'css' === this.getFormat(f) }
/**
@@ -151,15 +272,36 @@ Crsh.prototype.remove = function (f) {
return this
}
+/**
+ * Applies input filters to file map
+ */
+Crsh.prototype.applyInputFilters = function (files, cb) {
+ var self = this
+
+ // Prepare filters for each file
+ var filters = this.list.map(function (file) {
+ var fn = self.findFilter(path.extname(file))
+ return function (next) {
+ files[file] = files[file].toString()
+ fn(files[file], function (err, data) {
+ err || (files[file] = data)
+ next(err)
+ })
+ }
+ })
+
+ // Apply filters in parallel
+ async.parallel(filters, function (err) {
+ cb(err, files)
+ })
+}
+
/**
* Merge and minify all the files
*/
Crsh.prototype.compile = function (cb) {
- var isProd = process.env.NODE_ENV === 'production'
- , list = this.list
- , self = this
-
+ var list = this.list, self = this
cb || (cb = function () {})
// Generate name only when a compile occurs
@@ -169,45 +311,29 @@ Crsh.prototype.compile = function (cb) {
Files.load(list, function (err, files) {
if (err) cb(err)
- // Merge all the files
- var data = list.map(function (file) {
+ self.applyInputFilters(files, function (err, filteredFiles) {
+ if (err) { return cb(err) }
- // Compile coffeescript files
- if (path.extname(file) === '.coffee') {
- return coffee.compile(files[file].toString())
- }
+ // Merge all the files
+ var data = list.map(function (file) {
+ return filteredFiles[file]
+ }).join('')
- // Compile stylus files
- if (path.extname(file) === '.styl') {
- var css = ''
- stylus(files[file].toString())
- .set('compress', isProd)
- .set('filename', file)
- .render(function (err, v) {
- css = v
- })
-
- return css
+ // Only uglify in production
+ if (isProd && self.format === 'js') {
+ var ast = jsp.parse(data)
+ ast = pro.ast_mangle(ast)
+ ast = pro.ast_squeeze(ast)
+ data = pro.gen_code(ast)
}
- // Return normal files as-is
- return files[file]
- }).join('')
-
- // Only uglify in production
- if (isProd && self.format === 'js') {
- var ast = jsp.parse(data)
- ast = pro.ast_mangle(ast)
- ast = pro.ast_squeeze(ast)
- data = pro.gen_code(ast)
- }
-
- // Write merged and uglified data to file
- var p = path.join(self.path, self.name)
- Files.write(p, data, function (err) {
- if (err) return cb(err)
- self.lastChange = Date.now()
- cb()
+ // Write merged and uglified data to file
+ var p = path.join(self.path, self.name)
+ Files.write(p, data, function (err) {
+ if (err) return cb(err)
+ self.lastChange = Date.now()
+ cb()
+ })
})
})
}
@@ -271,5 +397,22 @@ Crsh.middleware = function (path, blocks) {
}
+// Cheat some prototype accessors into statics
+Crsh.addFilter = Crsh.prototype.addFilter.bind(Crsh.prototype)
+Crsh.findFilter = Crsh.prototype.findFilter.bind(Crsh.prototype)
+Crsh.addType = Crsh.prototype.addType.bind(Crsh.prototype)
+Crsh.isJs = Crsh.prototype.isJs.bind(Crsh.prototype)
+Crsh.isCss = Crsh.prototype.isCss.bind(Crsh.prototype)
+Crsh.getFormat = Crsh.prototype.getFormat.bind(Crsh.prototype)
+
+
+// For backwards compatibility,
+// enable coffee and stylus by default.
+process.nextTick(function () {
+ Crsh.addFilter('coffee', Crsh.coffee())
+ Crsh.addFilter('styl', Crsh.stylus())
+})
+
+
// Export the interface
module.exports = Crsh
View
4 package.json
@@ -2,7 +2,7 @@
"author": "Stephen Belanger <admin@stephenbelanger.com> (http://stephenbelanger.com)",
"name": "crsh",
"description": "crsh your javascript and css into tiny blocks.",
- "version": "0.1.1",
+ "version": "0.2.0",
"repository": {
"url": "git://github.com/Qard/crsh.git"
},
@@ -14,4 +14,4 @@
"uglify-js": "1.2.x"
},
"devDependencies": {}
-}
+}

0 comments on commit 2aef35a

Please sign in to comment.
Something went wrong with that request. Please try again.