/* content.coffee */ (function() { var ContentPlugin, ContentTree, StaticFile, async, chalk, fs, loadContent, minimatch, minimatchOptions, path, setImmediate, url, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty, slice = [].slice; async = require('async'); fs = require('fs'); path = require('path'); url = require('url'); chalk = require('chalk'); minimatch = require('minimatch'); minimatchOptions = { dot: false }; if (typeof setImmediate === "undefined" || setImmediate === null) { setImmediate = process.nextTick; } ContentPlugin = (function() { /* The mother of all plugins */ function ContentPlugin() {} ContentPlugin.property = function(name, getter) { /* Define read-only property with *name*. */ var get; if (typeof getter === 'string') { get = function() { return this[getter].call(this); }; } else { get = function() { return getter.call(this); }; } return Object.defineProperty(this.prototype, name, { get: get, enumerable: true }); }; ContentPlugin.property('view', 'getView'); ContentPlugin.prototype.getView = function() { /* 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.'); }; ContentPlugin.property('filename', 'getFilename'); ContentPlugin.prototype.getFilename = function() { /* Return filename for this content. This is where the result of the plugin's view will be written to. */ throw new Error('Not implemented.'); }; ContentPlugin.property('url', 'getUrl'); ContentPlugin.prototype.getUrl = function(base) { /* Return url for this content relative to *base*. */ var filename; filename = this.getFilename(); if (base == null) { base = this.__env.config.baseUrl; } if (!base.match(/\/$/)) { base += '/'; } if (process.platform === 'win32') { filename = filename.replace(/\\/g, '/'); } return url.resolve(base, filename); }; ContentPlugin.property('pluginColor', 'getPluginColor'); ContentPlugin.prototype.getPluginColor = function() { /* 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'; }; ContentPlugin.property('pluginInfo', 'getPluginInfo'); ContentPlugin.prototype.getPluginInfo = function() { /* Return plugin information. Also displayed in the content tree printout. */ return "url: " + this.url; }; return ContentPlugin; })(); ContentPlugin.fromFile = function(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.'); }; StaticFile = (function(superClass) { extend(StaticFile, superClass); /* Static file handler, simply serves content as-is. Last in chain. */ function StaticFile(filepath1) { this.filepath = filepath1; } StaticFile.prototype.getView = function() { return function() { var args, callback, error, j, rs; args = 2 <= arguments.length ? slice.call(arguments, 0, j = arguments.length - 1) : (j = 0, []), callback = arguments[j++]; try { rs = fs.createReadStream(this.filepath.full); } catch (error1) { error = error1; return callback(error); } return callback(null, rs); }; }; StaticFile.prototype.getFilename = function() { return this.filepath.relative; }; StaticFile.prototype.getPluginColor = function() { return 'none'; }; return StaticFile; })(ContentPlugin); StaticFile.fromFile = function(filepath, callback) { return callback(null, new StaticFile(filepath)); }; loadContent = function(env, filepath, callback) { /* Helper that loads content plugin found in *filepath*. */ var i, j, plugin, ref; env.logger.silly("loading " + filepath.relative); plugin = { "class": StaticFile, group: 'files' }; for (i = j = ref = env.contentPlugins.length - 1; j >= 0; i = j += -1) { if (minimatch(filepath.relative, env.contentPlugins[i].pattern, minimatchOptions)) { plugin = env.contentPlugins[i]; break; } } return plugin["class"].fromFile(filepath, function(error, instance) { if (error != null) { error.message = filepath.relative + ": " + error.message; } if (instance != null) { instance.__env = env; } if (instance != null) { instance.__plugin = plugin; } if (instance != null) { instance.__filename = filepath.full; } return callback(error, instance); }); }; ContentTree = function(filename, groupNames) { var groups, j, len, name, parent; if (groupNames == null) { groupNames = []; } parent = null; groups = { directories: [], files: [] }; for (j = 0, len = groupNames.length; j < len; j++) { name = groupNames[j]; groups[name] = []; } Object.defineProperty(this, '__groupNames', { get: function() { return groupNames; } }); Object.defineProperty(this, '_', { get: function() { return groups; } }); Object.defineProperty(this, 'filename', { get: function() { return filename; } }); Object.defineProperty(this, 'index', { get: function() { var item, key, ref; ref = this; for (key in ref) { item = ref[key]; if (key.slice(0, 6) === 'index.') { return item; } } } }); return Object.defineProperty(this, 'parent', { get: function() { return parent; }, set: function(val) { return parent = val; } }); }; ContentTree.fromDirectory = function(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. */ var createInstance, createInstances, filterIgnored, readDirectory, reldir, resolveFilenames, tree; reldir = env.relativeContentsPath(directory); tree = new ContentTree(reldir, env.getContentGroups()); env.logger.silly("creating content tree from " + directory); readDirectory = function(callback) { return fs.readdir(directory, callback); }; resolveFilenames = function(filenames, callback) { filenames.sort(); return async.map(filenames, function(filename, callback) { var relname; relname = path.join(reldir, filename); return callback(null, { full: path.join(env.contentsPath, relname), relative: relname }); }, callback); }; filterIgnored = function(filenames, callback) { /* Exclude *filenames* matching ignore patterns in environment config. */ if (env.config.ignore.length > 0) { return async.filter(filenames, function(filename, callback) { var include, j, len, pattern, ref; include = true; ref = env.config.ignore; for (j = 0, len = ref.length; j < len; j++) { pattern = ref[j]; if (minimatch(filename.relative, pattern, minimatchOptions)) { env.logger.verbose("ignoring " + filename.relative + " (matches: " + pattern + ")"); include = false; break; } } return callback(null, include); }, callback); } else { return callback(null, filenames); } }; createInstance = function(filepath, callback) { /* Create plugin or subtree instance for *filepath*. */ return setImmediate(function() { return async.waterfall([ async.apply(fs.stat, filepath.full), function(stats, callback) { var basename; basename = path.basename(filepath.relative); if (stats.isDirectory()) { return ContentTree.fromDirectory(env, filepath.full, function(error, result) { result.parent = tree; tree[basename] = result; tree._.directories.push(result); return callback(error); }); } else if (stats.isFile()) { return loadContent(env, filepath, function(error, instance) { if (!error) { instance.parent = tree; tree[basename] = instance; tree._[instance.__plugin.group].push(instance); } return callback(error); }); } else { return callback(new Error("Invalid file " + filepath.full + ".")); } } ], callback); }); }; createInstances = function(filenames, callback) { return async.forEachLimit(filenames, env.config._fileLimit, createInstance, callback); }; return async.waterfall([readDirectory, resolveFilenames, filterIgnored, createInstances], function(error) { return callback(error, tree); }); }; ContentTree.inspect = function(tree, depth) { var cfn, i, j, k, keys, l, len, pad, ref, rv, s, v; if (depth == null) { depth = 0; } /* Return a pretty formatted string representing the content *tree*. */ if (typeof tree === 'number') { return '[Function: ContentTree]'; } rv = []; pad = ''; for (i = j = 0, ref = depth; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) { pad += ' '; } keys = Object.keys(tree).sort(function(a, b) { var ad, bd; ad = tree[a] instanceof ContentTree; bd = tree[b] instanceof ContentTree; if (ad !== bd) { return bd - ad; } if (a < b) { return -1; } if (a > b) { return 1; } return 0; }); for (l = 0, len = keys.length; l < len; l++) { k = keys[l]; v = tree[k]; if (v instanceof ContentTree) { s = (chalk.bold(k)) + "/\n"; s += ContentTree.inspect(v, depth + 1); } else { cfn = function(s) { return s; }; if (v.pluginColor !== 'none') { if (!(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); } return rv.join('\n'); }; ContentTree.flatten = function(tree) { /* Return all the items in the *tree* as an array of content plugins. */ var key, rv, value; rv = []; for (key in tree) { value = tree[key]; if (value instanceof ContentTree) { rv = rv.concat(ContentTree.flatten(value)); } else { rv.push(value); } } return rv; }; ContentTree.merge = function(root, tree) { /* Merge *tree* into *root* tree. */ var item, key; for (key in tree) { item = tree[key]; if (item instanceof ContentPlugin) { root[key] = item; item.parent = root; root._[item.__plugin.group].push(item); } else if (item instanceof ContentTree) { if (root[key] == null) { 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 + "'"); } } }; /* Exports */ module.exports = { ContentTree: ContentTree, ContentPlugin: ContentPlugin, StaticFile: StaticFile, loadContent: loadContent }; }).call(this);