environment.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. /* environment.coffee */
  2. (function() {
  3. var Config, ContentPlugin, ContentTree, Environment, EventEmitter, StaticFile, TemplatePlugin, async, fs, loadTemplates, logger, path, readJSON, readJSONSync, ref, ref1, render, runGenerator, utils,
  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. indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
  8. path = require('path');
  9. async = require('async');
  10. fs = require('fs');
  11. EventEmitter = require('events').EventEmitter;
  12. utils = require('./utils');
  13. Config = require('./config').Config;
  14. ref = require('./content'), ContentPlugin = ref.ContentPlugin, ContentTree = ref.ContentTree, StaticFile = ref.StaticFile;
  15. ref1 = require('./templates'), TemplatePlugin = ref1.TemplatePlugin, loadTemplates = ref1.loadTemplates;
  16. logger = require('./logger').logger;
  17. render = require('./renderer').render;
  18. runGenerator = require('./generator').runGenerator;
  19. readJSON = utils.readJSON, readJSONSync = utils.readJSONSync;
  20. Environment = (function(superClass) {
  21. extend(Environment, superClass);
  22. /* The Wintersmith environment. */
  23. Environment.prototype.utils = utils;
  24. Environment.prototype.ContentTree = ContentTree;
  25. Environment.prototype.ContentPlugin = ContentPlugin;
  26. Environment.prototype.TemplatePlugin = TemplatePlugin;
  27. function Environment(config, workDir1, logger1) {
  28. this.workDir = workDir1;
  29. this.logger = logger1;
  30. /* Create a new Environment, *config* is a Config instance, *workDir* is the
  31. working directory and *logger* is a log instance implementing methods for
  32. error, warn, verbose and silly loglevels.
  33. */
  34. this.loadedModules = [];
  35. this.workDir = path.resolve(this.workDir);
  36. this.setConfig(config);
  37. this.reset();
  38. }
  39. Environment.prototype.reset = function() {
  40. /* Reset environment and clear any loaded modules from require.cache */
  41. var id;
  42. this.views = {
  43. none: function() {
  44. var args, callback, i;
  45. args = 2 <= arguments.length ? slice.call(arguments, 0, i = arguments.length - 1) : (i = 0, []), callback = arguments[i++];
  46. return callback();
  47. }
  48. };
  49. this.generators = [];
  50. this.plugins = {
  51. StaticFile: StaticFile
  52. };
  53. this.templatePlugins = [];
  54. this.contentPlugins = [];
  55. this.helpers = {};
  56. while (id = this.loadedModules.pop()) {
  57. this.logger.verbose("unloading: " + id);
  58. delete require.cache[id];
  59. }
  60. return this.setupLocals();
  61. };
  62. Environment.prototype.setConfig = function(config1) {
  63. this.config = config1;
  64. this.contentsPath = this.resolvePath(this.config.contents);
  65. return this.templatesPath = this.resolvePath(this.config.templates);
  66. };
  67. Environment.prototype.setupLocals = function() {
  68. /* Resolve locals and loads any required modules. */
  69. var alias, error, filename, id, ref2;
  70. this.locals = {};
  71. if (typeof this.config.locals === 'string') {
  72. filename = this.resolvePath(this.config.locals);
  73. this.logger.verbose("loading locals from: " + filename);
  74. this.locals = readJSONSync(filename);
  75. } else {
  76. this.locals = this.config.locals;
  77. }
  78. ref2 = this.config.require;
  79. for (alias in ref2) {
  80. id = ref2[alias];
  81. logger.verbose("loading module '" + id + "' available in locals as '" + alias + "'");
  82. if (this.locals[alias] != null) {
  83. logger.warn("module '" + id + "' overwrites previous local with the same key ('" + alias + "')");
  84. }
  85. try {
  86. this.locals[alias] = this.loadModule(id);
  87. } catch (error1) {
  88. error = error1;
  89. logger.warn("unable to load '" + id + "': " + error.message);
  90. }
  91. }
  92. };
  93. Environment.prototype.resolvePath = function(pathname) {
  94. /* Resolve *pathname* in working directory, returns an absolute path. */
  95. return path.resolve(this.workDir, pathname || '');
  96. };
  97. Environment.prototype.resolveContentsPath = function(pathname) {
  98. /* Resolve *pathname* in contents directory, returns an absolute path. */
  99. return path.resolve(this.contentsPath, pathname || '');
  100. };
  101. Environment.prototype.resolveModule = function(module) {
  102. /* Resolve *module* to an absolute path, mimicking the node.js module loading system. */
  103. var error, nodeDir;
  104. switch (module[0]) {
  105. case '.':
  106. return require.resolve(this.resolvePath(module));
  107. case '/':
  108. return require.resolve(module);
  109. default:
  110. nodeDir = this.resolvePath('node_modules');
  111. try {
  112. return require.resolve(path.join(nodeDir, module));
  113. } catch (error1) {
  114. error = error1;
  115. return require.resolve(module);
  116. }
  117. }
  118. };
  119. Environment.prototype.relativePath = function(pathname) {
  120. /* Resolve path relative to working directory. */
  121. return path.relative(this.workDir, pathname);
  122. };
  123. Environment.prototype.relativeContentsPath = function(pathname) {
  124. /* Resolve path relative to contents directory. */
  125. return path.relative(this.contentsPath, pathname);
  126. };
  127. Environment.prototype.registerContentPlugin = function(group, pattern, plugin) {
  128. /* Add a content *plugin* to the environment. Files in the contents directory
  129. matching the glob *pattern* will be instantiated using the plugin's `fromFile`
  130. factory method. The *group* argument is used to group the loaded instances under
  131. each directory. I.e. plugin instances with the group 'textFiles' can be found
  132. in `contents.somedir._.textFiles`.
  133. */
  134. this.logger.verbose("registering content plugin " + plugin.name + " that handles: " + pattern);
  135. this.plugins[plugin.name] = plugin;
  136. return this.contentPlugins.push({
  137. group: group,
  138. pattern: pattern,
  139. "class": plugin
  140. });
  141. };
  142. Environment.prototype.registerTemplatePlugin = function(pattern, plugin) {
  143. /* Add a template *plugin* to the environment. All files in the template directory
  144. matching the glob *pattern* will be passed to the plugin's `fromFile` classmethod.
  145. */
  146. this.logger.verbose("registering template plugin " + plugin.name + " that handles: " + pattern);
  147. this.plugins[plugin.name] = plugin;
  148. return this.templatePlugins.push({
  149. pattern: pattern,
  150. "class": plugin
  151. });
  152. };
  153. Environment.prototype.registerGenerator = function(group, generator) {
  154. /* Add a generator to the environment. The generator function is called with the env and the
  155. current content tree. It should return a object with nested ContentPlugin instances.
  156. These will be merged into the final content tree.
  157. */
  158. return this.generators.push({
  159. group: group,
  160. fn: generator
  161. });
  162. };
  163. Environment.prototype.registerView = function(name, view) {
  164. /* Add a view to the environment. */
  165. return this.views[name] = view;
  166. };
  167. Environment.prototype.getContentGroups = function() {
  168. /* Return an array of all registered content groups */
  169. var generator, groups, i, j, len, len1, plugin, ref2, ref3, ref4, ref5;
  170. groups = [];
  171. ref2 = this.contentPlugins;
  172. for (i = 0, len = ref2.length; i < len; i++) {
  173. plugin = ref2[i];
  174. if (ref3 = plugin.group, indexOf.call(groups, ref3) < 0) {
  175. groups.push(plugin.group);
  176. }
  177. }
  178. ref4 = this.generators;
  179. for (j = 0, len1 = ref4.length; j < len1; j++) {
  180. generator = ref4[j];
  181. if (ref5 = generator.group, indexOf.call(groups, ref5) < 0) {
  182. groups.push(generator.group);
  183. }
  184. }
  185. return groups;
  186. };
  187. Environment.prototype.loadModule = function(module, unloadOnReset) {
  188. var id, rv;
  189. if (unloadOnReset == null) {
  190. unloadOnReset = false;
  191. }
  192. /* Requires and returns *module*, resolved from the current working directory. */
  193. if (module.slice(-7) === '.coffee') {
  194. require('coffee-script/register');
  195. }
  196. this.logger.silly("loading module: " + module);
  197. id = this.resolveModule(module);
  198. this.logger.silly("resolved: " + id);
  199. rv = require(id);
  200. if (unloadOnReset) {
  201. this.loadedModules.push(id);
  202. }
  203. return rv;
  204. };
  205. Environment.prototype.loadPluginModule = function(module, callback) {
  206. /* Load a plugin *module*. Calls *callback* when plugin is done loading, or an error occurred. */
  207. var done, error, id;
  208. id = 'unknown';
  209. done = function(error) {
  210. if (error != null) {
  211. error.message = "Error loading plugin '" + id + "': " + error.message;
  212. }
  213. return callback(error);
  214. };
  215. if (typeof module === 'string') {
  216. id = module;
  217. try {
  218. module = this.loadModule(module);
  219. } catch (error1) {
  220. error = error1;
  221. done(error);
  222. return;
  223. }
  224. }
  225. try {
  226. return module.call(null, this, done);
  227. } catch (error1) {
  228. error = error1;
  229. return done(error);
  230. }
  231. };
  232. Environment.prototype.loadViewModule = function(id, callback) {
  233. /* Load a view *module* and add it to the environment. */
  234. var error, module;
  235. this.logger.verbose("loading view: " + id);
  236. try {
  237. module = this.loadModule(id, true);
  238. } catch (error1) {
  239. error = error1;
  240. error.message = "Error loading view '" + id + "': " + error.message;
  241. callback(error);
  242. return;
  243. }
  244. this.registerView(path.basename(id), module);
  245. return callback();
  246. };
  247. Environment.prototype.loadPlugins = function(callback) {
  248. /* Loads any plugin found in *@config.plugins*. */
  249. return async.series([
  250. (function(_this) {
  251. return function(callback) {
  252. return async.forEachSeries(_this.constructor.defaultPlugins, function(plugin, callback) {
  253. var id, module;
  254. _this.logger.verbose("loading default plugin: " + plugin);
  255. id = require.resolve("./../plugins/" + plugin);
  256. module = require(id);
  257. _this.loadedModules.push(id);
  258. return _this.loadPluginModule(module, callback);
  259. }, callback);
  260. };
  261. })(this), (function(_this) {
  262. return function(callback) {
  263. return async.forEachSeries(_this.config.plugins, function(plugin, callback) {
  264. _this.logger.verbose("loading plugin: " + plugin);
  265. return _this.loadPluginModule(plugin, callback);
  266. }, callback);
  267. };
  268. })(this)
  269. ], callback);
  270. };
  271. Environment.prototype.loadViews = function(callback) {
  272. /* Loads files found in the *@config.views* directory and registers them as views. */
  273. if (this.config.views == null) {
  274. return callback();
  275. }
  276. return async.waterfall([
  277. (function(_this) {
  278. return function(callback) {
  279. return fs.readdir(_this.resolvePath(_this.config.views), callback);
  280. };
  281. })(this), (function(_this) {
  282. return function(filenames, callback) {
  283. var modules;
  284. modules = filenames.map(function(filename) {
  285. return _this.config.views + "/" + filename;
  286. });
  287. return async.forEach(modules, _this.loadViewModule.bind(_this), callback);
  288. };
  289. })(this)
  290. ], callback);
  291. };
  292. Environment.prototype.getContents = function(callback) {
  293. /* Build the ContentTree from *@contentsPath*, also runs any registered generators. */
  294. return async.waterfall([
  295. (function(_this) {
  296. return function(callback) {
  297. return ContentTree.fromDirectory(_this, _this.contentsPath, callback);
  298. };
  299. })(this), (function(_this) {
  300. return function(contents, callback) {
  301. return async.mapSeries(_this.generators, function(generator, callback) {
  302. return runGenerator(_this, contents, generator, callback);
  303. }, function(error, generated) {
  304. var gentree, i, len, tree;
  305. if ((error != null) || generated.length === 0) {
  306. return callback(error, contents);
  307. }
  308. try {
  309. tree = new ContentTree('', _this.getContentGroups());
  310. for (i = 0, len = generated.length; i < len; i++) {
  311. gentree = generated[i];
  312. ContentTree.merge(tree, gentree);
  313. }
  314. ContentTree.merge(tree, contents);
  315. } catch (error1) {
  316. error = error1;
  317. return callback(error);
  318. }
  319. return callback(null, tree);
  320. });
  321. };
  322. })(this)
  323. ], callback);
  324. };
  325. Environment.prototype.getTemplates = function(callback) {
  326. /* Load templates. */
  327. return loadTemplates(this, callback);
  328. };
  329. Environment.prototype.getLocals = function(callback) {
  330. /* Returns locals. */
  331. return callback(null, this.locals);
  332. };
  333. Environment.prototype.load = function(callback) {
  334. /* Convenience method to load plugins, views, contents, templates and locals. */
  335. return async.waterfall([
  336. (function(_this) {
  337. return function(callback) {
  338. return async.parallel([
  339. function(callback) {
  340. return _this.loadPlugins(callback);
  341. }, function(callback) {
  342. return _this.loadViews(callback);
  343. }
  344. ], callback);
  345. };
  346. })(this), (function(_this) {
  347. return function(_, callback) {
  348. return async.parallel({
  349. contents: function(callback) {
  350. return _this.getContents(callback);
  351. },
  352. templates: function(callback) {
  353. return _this.getTemplates(callback);
  354. },
  355. locals: function(callback) {
  356. return _this.getLocals(callback);
  357. }
  358. }, callback);
  359. };
  360. })(this)
  361. ], callback);
  362. };
  363. Environment.prototype.preview = function(callback) {
  364. /* Start the preview server. Calls *callback* with the server instance when it is up and
  365. running or if an error occurs. NOTE: The returned server instance will be invalid if the
  366. config file changes and the server is restarted because of it. As a temporary workaround
  367. you can set the _restartOnConfChange key in settings to false.
  368. */
  369. var server;
  370. this.mode = 'preview';
  371. server = require('./server');
  372. return server.run(this, callback);
  373. };
  374. Environment.prototype.build = function(outputDir, callback) {
  375. /* Build the content tree and render it to *outputDir*. */
  376. this.mode = 'build';
  377. if (arguments.length < 2) {
  378. callback = outputDir || function() {};
  379. outputDir = this.resolvePath(this.config.output);
  380. }
  381. return async.waterfall([
  382. (function(_this) {
  383. return function(callback) {
  384. return _this.load(callback);
  385. };
  386. })(this), (function(_this) {
  387. return function(result, callback) {
  388. var contents, locals, templates;
  389. contents = result.contents, templates = result.templates, locals = result.locals;
  390. return render(_this, outputDir, contents, templates, locals, callback);
  391. };
  392. })(this)
  393. ], callback);
  394. };
  395. return Environment;
  396. })(EventEmitter);
  397. Environment.create = function(config, workDir, log) {
  398. if (log == null) {
  399. log = logger;
  400. }
  401. /* Set up a new environment using the default logger, *config* can be
  402. either a config object, a Config instance or a path to a config file.
  403. */
  404. if (typeof config === 'string') {
  405. if (workDir == null) {
  406. workDir = path.dirname(config);
  407. }
  408. config = Config.fromFileSync(config);
  409. } else {
  410. if (workDir == null) {
  411. workDir = process.cwd();
  412. }
  413. if (!(config instanceof Config)) {
  414. config = new Config(config);
  415. }
  416. }
  417. return new Environment(config, workDir, log);
  418. };
  419. Environment.defaultPlugins = ['page', 'pug', 'markdown'];
  420. /* Exports */
  421. module.exports = {
  422. Environment: Environment
  423. };
  424. }).call(this);