Permalink
295 lines (248 sloc) 9.56 KB
### content.coffee ###
async = require 'async'
fs = require 'fs'
path = require 'path'
url = require 'url'
chalk = require 'chalk'
minimatch = require 'minimatch'
# options passed to minimatch for ignore and plugin matching
minimatchOptions =
dot: false
if not setImmediate?
setImmediate = process.nextTick
class ContentPlugin
### The mother of all plugins ###
@property = (name, getter) ->
### Define read-only property with *name*. ###
if typeof getter is 'string'
get = -> this[getter].call this
else
get = -> getter.call this
Object.defineProperty @prototype, name,
get: get
enumerable: true
@property 'view', 'getView'
getView: ->
### Return a view that renders the plugin. Either a string naming a exisitng view or a function:
`(env, locals, contents, templates, callback) ->`
Where *environment* is the current wintersmith environment, *contents* is the content-tree
and *templates* is a map of all templates as: {filename: templateInstance}. *callback* should be
called with a stream/buffer or null if this plugin instance should not be rendered. ###
throw new Error 'Not implemented.'
@property 'filename', 'getFilename'
getFilename: ->
### Return filename for this content. This is where the result of the plugin's view will be written to. ###
throw new Error 'Not implemented.'
@property 'url', 'getUrl'
getUrl: (base) ->
### Return url for this content relative to *base*. ###
filename = @getFilename()
base ?= @__env.config.baseUrl
if not base.match /\/$/
base += '/'
if process.platform is 'win32'
filename = filename.replace /\\/g, '/' #'
return url.resolve base, filename
@property 'pluginColor', 'getPluginColor'
getPluginColor: ->
### Return vanity color used to identify the plugin when printing the content tree
choices are: bold, italic, underline, inverse, yellow, cyan, white, magenta,
green, red, grey, blue, rainbow, zebra or none. ###
return 'cyan'
@property 'pluginInfo', 'getPluginInfo'
getPluginInfo: ->
### Return plugin information. Also displayed in the content tree printout. ###
return "url: #{ @url }"
ContentPlugin.fromFile = (filepath, callback) ->
### Calls *callback* with an instance of class. Where *filepath* is an object containing
both the absolute and realative paths for the file. e.g.
{full: "/home/foo/mysite/contents/somedir/somefile.ext",
relative: "somedir/somefile.ext"} ###
throw new Error 'Not implemented.'
class StaticFile extends ContentPlugin
### Static file handler, simply serves content as-is. Last in chain. ###
constructor: (@filepath) ->
getView: ->
return (args..., callback) ->
# locals, contents etc not used in this plugin
try
rs = fs.createReadStream @filepath.full
catch error
return callback error
callback null, rs
getFilename: ->
@filepath.relative
getPluginColor: ->
'none'
StaticFile.fromFile = (filepath, callback) ->
# normally you would want to read the file here, the static plugin however
# just pipes it to the file/http response
callback null, new StaticFile(filepath)
loadContent = (env, filepath, callback) ->
### Helper that loads content plugin found in *filepath*. ###
env.logger.silly "loading #{ filepath.relative }"
# any file not matched to a plugin will be handled by the static file plug
plugin =
class: StaticFile
group: 'files'
# iterate backwards over all content plugins and check if any plugin can handle this file
for i in [env.contentPlugins.length - 1..0] by -1
if minimatch filepath.relative, env.contentPlugins[i].pattern, minimatchOptions
plugin = env.contentPlugins[i]
break
# have the plugin's factory method create our instance
plugin.class.fromFile filepath, (error, instance) ->
error.message = "#{ filepath.relative }: #{ error.message }" if error?
# keep some references to the plugin and file used to create this instance
instance?.__env = env
instance?.__plugin = plugin
instance?.__filename = filepath.full
callback error, instance
# Class ContentTree
# not using Class since we need a clean prototype
ContentTree = (filename, groupNames=[]) ->
parent = null
groups = {directories: [], files: []}
for name in groupNames
groups[name] = []
Object.defineProperty this, '__groupNames',
get: -> groupNames
Object.defineProperty this, '_',
get: -> groups
Object.defineProperty this, 'filename',
get: -> filename
Object.defineProperty this, 'index',
get: ->
for key, item of this
if key[0...6] is 'index.'
return item
return
Object.defineProperty this, 'parent',
get: -> parent
set: (val) -> parent = val
ContentTree.fromDirectory = (env, directory, callback) ->
### Recursively scan *directory* and build a ContentTree with enviroment *env*.
Calls *callback* with a nested ContentTree or an error if something went wrong. ###
reldir = env.relativeContentsPath directory
tree = new ContentTree reldir, env.getContentGroups()
env.logger.silly "creating content tree from #{ directory }"
readDirectory = (callback) ->
fs.readdir directory, callback
resolveFilenames = (filenames, callback) ->
filenames.sort()
async.map filenames, (filename, callback) ->
relname = path.join reldir, filename
callback null,
full: path.join env.contentsPath, relname
relative: relname
, callback
filterIgnored = (filenames, callback) ->
### Exclude *filenames* matching ignore patterns in environment config. ###
if env.config.ignore.length > 0
async.filter filenames, (filename, callback) ->
include = true
for pattern in env.config.ignore
if minimatch filename.relative, pattern, minimatchOptions
env.logger.verbose "ignoring #{ filename.relative } (matches: #{ pattern })"
include = false
break
callback null, include
, callback
else
callback null, filenames
createInstance = (filepath, callback) ->
### Create plugin or subtree instance for *filepath*. ###
setImmediate ->
async.waterfall [
async.apply fs.stat, filepath.full
(stats, callback) ->
basename = path.basename filepath.relative
# recursively map directories to content tree instances
if stats.isDirectory()
ContentTree.fromDirectory env, filepath.full, (error, result) ->
result.parent = tree
tree[basename] = result
tree._.directories.push result # add instance to the directory group of its parent
callback error
# map files to content plugins
else if stats.isFile()
loadContent env, filepath, (error, instance) ->
if not error
instance.parent = tree
tree[basename] = instance
tree._[instance.__plugin.group].push instance
callback error
# This should never happen™
else
callback new Error "Invalid file #{ filepath.full }."
], callback
createInstances = (filenames, callback) ->
# NOTE: the file limit is not really enforced here since this is a recursive function
# but won't be a problem in 99% of cases, patches welcome :-)
# TODO: actually this has to be fixed, we want an error to stop instance creation
async.forEachLimit filenames, env.config._fileLimit, createInstance, callback
async.waterfall [
readDirectory
resolveFilenames
filterIgnored
createInstances
], (error) ->
callback error, tree
ContentTree.inspect = (tree, depth=0) ->
### Return a pretty formatted string representing the content *tree*. ###
if typeof tree is 'number'
# workaround for node.js calling inspect when converting objects to a string
return '[Function: ContentTree]'
rv = []
pad = ''
for i in [0..depth]
pad += ' '
keys = Object.keys(tree).sort (a, b) ->
# sort items by type and name to keep directories on top
ad = tree[a] instanceof ContentTree
bd = tree[b] instanceof ContentTree
return bd - ad if ad isnt bd
return -1 if a < b
return 1 if a > b
return 0
for k in keys
v = tree[k]
if v instanceof ContentTree
s = "#{ chalk.bold k }/\n"
s += ContentTree.inspect v, depth + 1
else
cfn = (s) -> s
if v.pluginColor isnt 'none'
unless cfn = chalk[v.pluginColor]
throw new Error "Plugin #{ k } specifies invalid pluginColor: #{ v.pluginColor }"
s = "#{ cfn k } (#{ chalk.grey v.pluginInfo })"
rv.push pad + s
rv.join '\n'
ContentTree.flatten = (tree) ->
### Return all the items in the *tree* as an array of content plugins. ###
rv = []
for key, value of tree
if value instanceof ContentTree
rv = rv.concat ContentTree.flatten value
else
rv.push value
return rv
ContentTree.merge = (root, tree) ->
### Merge *tree* into *root* tree. ###
for key, item of tree
if item instanceof ContentPlugin
root[key] = item
item.parent = root
root._[item.__plugin.group].push item
else if item instanceof ContentTree
if not root[key]?
root[key] = new ContentTree key, item.__groupNames
root[key].parent = root
root[key].parent._.directories.push root[key]
if root[key] instanceof ContentTree
ContentTree.merge root[key], item
else
throw new Error "Invalid item in tree for '#{ key }'"
return
### Exports ###
module.exports = {ContentTree, ContentPlugin, StaticFile, loadContent}