page.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. (function() {
  2. var async, path, replaceAll, slugify,
  3. 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; },
  4. hasProp = {}.hasOwnProperty;
  5. path = require('path');
  6. async = require('async');
  7. slugify = require('slugg');
  8. replaceAll = function(string, map) {
  9. var re;
  10. re = new RegExp(Object.keys(map).join('|'), 'gi');
  11. return string.replace(re, function(match) {
  12. return map[match];
  13. });
  14. };
  15. module.exports = function(env, callback) {
  16. var Page, templateView;
  17. templateView = function(env, locals, contents, templates, callback) {
  18. /* Content view that expects content to have a @template instance var that
  19. matches a template in *templates*. Calls *callback* with output of template
  20. or null if @template is set to 'none'.
  21. */
  22. var ctx, template;
  23. if (this.template === 'none') {
  24. return callback(null, null);
  25. }
  26. template = templates[path.normalize(this.template)];
  27. if (template == null) {
  28. callback(new Error("page '" + this.filename + "' specifies unknown template '" + this.template + "'"));
  29. return;
  30. }
  31. ctx = {
  32. page: this
  33. };
  34. env.utils.extend(ctx, locals);
  35. return template.render(ctx, callback);
  36. };
  37. Page = (function(superClass) {
  38. extend(Page, superClass);
  39. /* Page base class, a page is content that has metadata, html and a template that renders it */
  40. function Page(filepath, metadata) {
  41. this.filepath = filepath;
  42. this.metadata = metadata;
  43. }
  44. Page.prototype.getFilename = function() {
  45. /* Returns the filename for this page based on the filename template.
  46. The default template (filenameTemplate config key) is ':file.html'.
  47. Available variables:
  48. :year - Full year from page.date
  49. :month - Zero-padded month from page.date
  50. :day - Zero-padded day from page.date
  51. :title - Slugified version of page.title
  52. :basename - filename from @filepath
  53. :file - basename without file extension
  54. :ext - file extension
  55. You can also run javascript by wrapping it in double moustaches {{ }}, in that context
  56. this page instance is available as *page* and the environment as *env*.
  57. Examples:
  58. (for a page with the filename somedir/myfile.md and date set to 2001-02-03)
  59. template: :file.html (default)
  60. output: somedir/myfile.html
  61. template: /:year/:month/:day/index.html
  62. output: 2001/02/03/index.html
  63. template: :year-:title.html
  64. output: somedir/2001-slugified-title.html
  65. template: /otherdir/{{ page.metadata.category }}/:basename
  66. output: otherdir/the-category/myfile.md
  67. */
  68. var basename, ctx, dirname, ext, file, filename, template, vm;
  69. template = this.filenameTemplate;
  70. dirname = path.dirname(this.filepath.relative);
  71. basename = path.basename(this.filepath.relative);
  72. file = env.utils.stripExtension(basename);
  73. ext = path.extname(basename);
  74. filename = replaceAll(template, {
  75. ':year': this.date.getFullYear(),
  76. ':month': ('0' + (this.date.getMonth() + 1)).slice(-2),
  77. ':day': ('0' + this.date.getDate()).slice(-2),
  78. ':title': slugify(this.title + ''),
  79. ':file': file,
  80. ':ext': ext,
  81. ':basename': basename,
  82. ':dirname': dirname
  83. });
  84. vm = ctx = null;
  85. filename = filename.replace(/\{\{(.*?)\}\}/g, (function(_this) {
  86. return function(match, code) {
  87. if (vm == null) {
  88. vm = require('vm');
  89. }
  90. if (ctx == null) {
  91. ctx = vm.createContext({
  92. env: env,
  93. page: _this
  94. });
  95. }
  96. return vm.runInContext(code, ctx);
  97. };
  98. })(this));
  99. if (filename[0] === '/') {
  100. return filename.slice(1);
  101. } else {
  102. return path.join(dirname, filename);
  103. }
  104. };
  105. Page.prototype.getUrl = function(base) {
  106. return Page.__super__.getUrl.call(this, base).replace(/([\/^])index\.html$/, '$1');
  107. };
  108. Page.prototype.getView = function() {
  109. return this.metadata.view || 'template';
  110. };
  111. /* Page specific properties */
  112. Page.property('html', 'getHtml');
  113. Page.prototype.getHtml = function(base) {
  114. if (base == null) {
  115. base = env.config.baseUrl;
  116. }
  117. /* return html with all urls resolved using *base* */
  118. throw new Error('Not implemented.');
  119. };
  120. Page.property('intro', 'getIntro');
  121. Page.prototype.getIntro = function(base) {
  122. var cutoff, cutoffs, html, i, idx, j, len;
  123. html = this.getHtml(base);
  124. cutoffs = env.config.introCutoffs || ['<span class="more', '<h2', '<hr'];
  125. idx = 2e308;
  126. for (j = 0, len = cutoffs.length; j < len; j++) {
  127. cutoff = cutoffs[j];
  128. i = html.indexOf(cutoff);
  129. if (i !== -1 && i < idx) {
  130. idx = i;
  131. }
  132. }
  133. if (idx !== 2e308) {
  134. return html.substr(0, idx);
  135. } else {
  136. return html;
  137. }
  138. };
  139. Page.property('filenameTemplate', 'getFilenameTemplate');
  140. Page.prototype.getFilenameTemplate = function() {
  141. return this.metadata.filename || env.config.filenameTemplate || ':file.html';
  142. };
  143. /* Template property used by the 'template' view */
  144. Page.property('template', 'getTemplate');
  145. Page.prototype.getTemplate = function() {
  146. return this.metadata.template || env.config.defaultTemplate || 'none';
  147. };
  148. Page.property('title', function() {
  149. return this.metadata.title || 'Untitled';
  150. });
  151. Page.property('date', function() {
  152. return new Date(this.metadata.date || 0);
  153. });
  154. Page.property('rfc822date', function() {
  155. return env.utils.rfc822(this.date);
  156. });
  157. Page.property('hasMore', function() {
  158. if (this._html == null) {
  159. this._html = this.getHtml();
  160. }
  161. if (this._intro == null) {
  162. this._intro = this.getIntro();
  163. }
  164. if (this._hasMore == null) {
  165. this._hasMore = this._html.length > this._intro.length;
  166. }
  167. return this._hasMore;
  168. });
  169. return Page;
  170. })(env.ContentPlugin);
  171. env.plugins.Page = Page;
  172. env.registerView('template', templateView);
  173. return callback();
  174. };
  175. }).call(this);