content.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. /* content.coffee */
  2. (function() {
  3. var ContentPlugin, ContentTree, StaticFile, async, chalk, fs, loadContent, minimatch, minimatchOptions, path, setImmediate, url,
  4. 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; },
  5. hasProp = {}.hasOwnProperty,
  6. slice = [].slice;
  7. async = require('async');
  8. fs = require('fs');
  9. path = require('path');
  10. url = require('url');
  11. chalk = require('chalk');
  12. minimatch = require('minimatch');
  13. minimatchOptions = {
  14. dot: false
  15. };
  16. if (typeof setImmediate === "undefined" || setImmediate === null) {
  17. setImmediate = process.nextTick;
  18. }
  19. ContentPlugin = (function() {
  20. /* The mother of all plugins */
  21. function ContentPlugin() {}
  22. ContentPlugin.property = function(name, getter) {
  23. /* Define read-only property with *name*. */
  24. var get;
  25. if (typeof getter === 'string') {
  26. get = function() {
  27. return this[getter].call(this);
  28. };
  29. } else {
  30. get = function() {
  31. return getter.call(this);
  32. };
  33. }
  34. return Object.defineProperty(this.prototype, name, {
  35. get: get,
  36. enumerable: true
  37. });
  38. };
  39. ContentPlugin.property('view', 'getView');
  40. ContentPlugin.prototype.getView = function() {
  41. /* Return a view that renders the plugin. Either a string naming a exisitng view or a function:
  42. `(env, locals, contents, templates, callback) ->`
  43. Where *environment* is the current wintersmith environment, *contents* is the content-tree
  44. and *templates* is a map of all templates as: {filename: templateInstance}. *callback* should be
  45. called with a stream/buffer or null if this plugin instance should not be rendered.
  46. */
  47. throw new Error('Not implemented.');
  48. };
  49. ContentPlugin.property('filename', 'getFilename');
  50. ContentPlugin.prototype.getFilename = function() {
  51. /* Return filename for this content. This is where the result of the plugin's view will be written to. */
  52. throw new Error('Not implemented.');
  53. };
  54. ContentPlugin.property('url', 'getUrl');
  55. ContentPlugin.prototype.getUrl = function(base) {
  56. /* Return url for this content relative to *base*. */
  57. var filename;
  58. filename = this.getFilename();
  59. if (base == null) {
  60. base = this.__env.config.baseUrl;
  61. }
  62. if (!base.match(/\/$/)) {
  63. base += '/';
  64. }
  65. if (process.platform === 'win32') {
  66. filename = filename.replace(/\\/g, '/');
  67. }
  68. return url.resolve(base, filename);
  69. };
  70. ContentPlugin.property('pluginColor', 'getPluginColor');
  71. ContentPlugin.prototype.getPluginColor = function() {
  72. /* Return vanity color used to identify the plugin when printing the content tree
  73. choices are: bold, italic, underline, inverse, yellow, cyan, white, magenta,
  74. green, red, grey, blue, rainbow, zebra or none.
  75. */
  76. return 'cyan';
  77. };
  78. ContentPlugin.property('pluginInfo', 'getPluginInfo');
  79. ContentPlugin.prototype.getPluginInfo = function() {
  80. /* Return plugin information. Also displayed in the content tree printout. */
  81. return "url: " + this.url;
  82. };
  83. return ContentPlugin;
  84. })();
  85. ContentPlugin.fromFile = function(filepath, callback) {
  86. /* Calls *callback* with an instance of class. Where *filepath* is an object containing
  87. both the absolute and realative paths for the file. e.g.
  88. {full: "/home/foo/mysite/contents/somedir/somefile.ext",
  89. relative: "somedir/somefile.ext"}
  90. */
  91. throw new Error('Not implemented.');
  92. };
  93. StaticFile = (function(superClass) {
  94. extend(StaticFile, superClass);
  95. /* Static file handler, simply serves content as-is. Last in chain. */
  96. function StaticFile(filepath1) {
  97. this.filepath = filepath1;
  98. }
  99. StaticFile.prototype.getView = function() {
  100. return function() {
  101. var args, callback, error, j, rs;
  102. args = 2 <= arguments.length ? slice.call(arguments, 0, j = arguments.length - 1) : (j = 0, []), callback = arguments[j++];
  103. try {
  104. rs = fs.createReadStream(this.filepath.full);
  105. } catch (error1) {
  106. error = error1;
  107. return callback(error);
  108. }
  109. return callback(null, rs);
  110. };
  111. };
  112. StaticFile.prototype.getFilename = function() {
  113. return this.filepath.relative;
  114. };
  115. StaticFile.prototype.getPluginColor = function() {
  116. return 'none';
  117. };
  118. return StaticFile;
  119. })(ContentPlugin);
  120. StaticFile.fromFile = function(filepath, callback) {
  121. return callback(null, new StaticFile(filepath));
  122. };
  123. loadContent = function(env, filepath, callback) {
  124. /* Helper that loads content plugin found in *filepath*. */
  125. var i, j, plugin, ref;
  126. env.logger.silly("loading " + filepath.relative);
  127. plugin = {
  128. "class": StaticFile,
  129. group: 'files'
  130. };
  131. for (i = j = ref = env.contentPlugins.length - 1; j >= 0; i = j += -1) {
  132. if (minimatch(filepath.relative, env.contentPlugins[i].pattern, minimatchOptions)) {
  133. plugin = env.contentPlugins[i];
  134. break;
  135. }
  136. }
  137. return plugin["class"].fromFile(filepath, function(error, instance) {
  138. if (error != null) {
  139. error.message = filepath.relative + ": " + error.message;
  140. }
  141. if (instance != null) {
  142. instance.__env = env;
  143. }
  144. if (instance != null) {
  145. instance.__plugin = plugin;
  146. }
  147. if (instance != null) {
  148. instance.__filename = filepath.full;
  149. }
  150. return callback(error, instance);
  151. });
  152. };
  153. ContentTree = function(filename, groupNames) {
  154. var groups, j, len, name, parent;
  155. if (groupNames == null) {
  156. groupNames = [];
  157. }
  158. parent = null;
  159. groups = {
  160. directories: [],
  161. files: []
  162. };
  163. for (j = 0, len = groupNames.length; j < len; j++) {
  164. name = groupNames[j];
  165. groups[name] = [];
  166. }
  167. Object.defineProperty(this, '__groupNames', {
  168. get: function() {
  169. return groupNames;
  170. }
  171. });
  172. Object.defineProperty(this, '_', {
  173. get: function() {
  174. return groups;
  175. }
  176. });
  177. Object.defineProperty(this, 'filename', {
  178. get: function() {
  179. return filename;
  180. }
  181. });
  182. Object.defineProperty(this, 'index', {
  183. get: function() {
  184. var item, key, ref;
  185. ref = this;
  186. for (key in ref) {
  187. item = ref[key];
  188. if (key.slice(0, 6) === 'index.') {
  189. return item;
  190. }
  191. }
  192. }
  193. });
  194. return Object.defineProperty(this, 'parent', {
  195. get: function() {
  196. return parent;
  197. },
  198. set: function(val) {
  199. return parent = val;
  200. }
  201. });
  202. };
  203. ContentTree.fromDirectory = function(env, directory, callback) {
  204. /* Recursively scan *directory* and build a ContentTree with enviroment *env*.
  205. Calls *callback* with a nested ContentTree or an error if something went wrong.
  206. */
  207. var createInstance, createInstances, filterIgnored, readDirectory, reldir, resolveFilenames, tree;
  208. reldir = env.relativeContentsPath(directory);
  209. tree = new ContentTree(reldir, env.getContentGroups());
  210. env.logger.silly("creating content tree from " + directory);
  211. readDirectory = function(callback) {
  212. return fs.readdir(directory, callback);
  213. };
  214. resolveFilenames = function(filenames, callback) {
  215. filenames.sort();
  216. return async.map(filenames, function(filename, callback) {
  217. var relname;
  218. relname = path.join(reldir, filename);
  219. return callback(null, {
  220. full: path.join(env.contentsPath, relname),
  221. relative: relname
  222. });
  223. }, callback);
  224. };
  225. filterIgnored = function(filenames, callback) {
  226. /* Exclude *filenames* matching ignore patterns in environment config. */
  227. if (env.config.ignore.length > 0) {
  228. return async.filter(filenames, function(filename, callback) {
  229. var include, j, len, pattern, ref;
  230. include = true;
  231. ref = env.config.ignore;
  232. for (j = 0, len = ref.length; j < len; j++) {
  233. pattern = ref[j];
  234. if (minimatch(filename.relative, pattern, minimatchOptions)) {
  235. env.logger.verbose("ignoring " + filename.relative + " (matches: " + pattern + ")");
  236. include = false;
  237. break;
  238. }
  239. }
  240. return callback(null, include);
  241. }, callback);
  242. } else {
  243. return callback(null, filenames);
  244. }
  245. };
  246. createInstance = function(filepath, callback) {
  247. /* Create plugin or subtree instance for *filepath*. */
  248. return setImmediate(function() {
  249. return async.waterfall([
  250. async.apply(fs.stat, filepath.full), function(stats, callback) {
  251. var basename;
  252. basename = path.basename(filepath.relative);
  253. if (stats.isDirectory()) {
  254. return ContentTree.fromDirectory(env, filepath.full, function(error, result) {
  255. result.parent = tree;
  256. tree[basename] = result;
  257. tree._.directories.push(result);
  258. return callback(error);
  259. });
  260. } else if (stats.isFile()) {
  261. return loadContent(env, filepath, function(error, instance) {
  262. if (!error) {
  263. instance.parent = tree;
  264. tree[basename] = instance;
  265. tree._[instance.__plugin.group].push(instance);
  266. }
  267. return callback(error);
  268. });
  269. } else {
  270. return callback(new Error("Invalid file " + filepath.full + "."));
  271. }
  272. }
  273. ], callback);
  274. });
  275. };
  276. createInstances = function(filenames, callback) {
  277. return async.forEachLimit(filenames, env.config._fileLimit, createInstance, callback);
  278. };
  279. return async.waterfall([readDirectory, resolveFilenames, filterIgnored, createInstances], function(error) {
  280. return callback(error, tree);
  281. });
  282. };
  283. ContentTree.inspect = function(tree, depth) {
  284. var cfn, i, j, k, keys, l, len, pad, ref, rv, s, v;
  285. if (depth == null) {
  286. depth = 0;
  287. }
  288. /* Return a pretty formatted string representing the content *tree*. */
  289. if (typeof tree === 'number') {
  290. return '[Function: ContentTree]';
  291. }
  292. rv = [];
  293. pad = '';
  294. for (i = j = 0, ref = depth; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) {
  295. pad += ' ';
  296. }
  297. keys = Object.keys(tree).sort(function(a, b) {
  298. var ad, bd;
  299. ad = tree[a] instanceof ContentTree;
  300. bd = tree[b] instanceof ContentTree;
  301. if (ad !== bd) {
  302. return bd - ad;
  303. }
  304. if (a < b) {
  305. return -1;
  306. }
  307. if (a > b) {
  308. return 1;
  309. }
  310. return 0;
  311. });
  312. for (l = 0, len = keys.length; l < len; l++) {
  313. k = keys[l];
  314. v = tree[k];
  315. if (v instanceof ContentTree) {
  316. s = (chalk.bold(k)) + "/\n";
  317. s += ContentTree.inspect(v, depth + 1);
  318. } else {
  319. cfn = function(s) {
  320. return s;
  321. };
  322. if (v.pluginColor !== 'none') {
  323. if (!(cfn = chalk[v.pluginColor])) {
  324. throw new Error("Plugin " + k + " specifies invalid pluginColor: " + v.pluginColor);
  325. }
  326. }
  327. s = (cfn(k)) + " (" + (chalk.grey(v.pluginInfo)) + ")";
  328. }
  329. rv.push(pad + s);
  330. }
  331. return rv.join('\n');
  332. };
  333. ContentTree.flatten = function(tree) {
  334. /* Return all the items in the *tree* as an array of content plugins. */
  335. var key, rv, value;
  336. rv = [];
  337. for (key in tree) {
  338. value = tree[key];
  339. if (value instanceof ContentTree) {
  340. rv = rv.concat(ContentTree.flatten(value));
  341. } else {
  342. rv.push(value);
  343. }
  344. }
  345. return rv;
  346. };
  347. ContentTree.merge = function(root, tree) {
  348. /* Merge *tree* into *root* tree. */
  349. var item, key;
  350. for (key in tree) {
  351. item = tree[key];
  352. if (item instanceof ContentPlugin) {
  353. root[key] = item;
  354. item.parent = root;
  355. root._[item.__plugin.group].push(item);
  356. } else if (item instanceof ContentTree) {
  357. if (root[key] == null) {
  358. root[key] = new ContentTree(key, item.__groupNames);
  359. root[key].parent = root;
  360. root[key].parent._.directories.push(root[key]);
  361. }
  362. if (root[key] instanceof ContentTree) {
  363. ContentTree.merge(root[key], item);
  364. }
  365. } else {
  366. throw new Error("Invalid item in tree for '" + key + "'");
  367. }
  368. }
  369. };
  370. /* Exports */
  371. module.exports = {
  372. ContentTree: ContentTree,
  373. ContentPlugin: ContentPlugin,
  374. StaticFile: StaticFile,
  375. loadContent: loadContent
  376. };
  377. }).call(this);