view.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. 'use strict';
  2. const { dirname, extname, join } = require('path');
  3. const { parse: yfm } = require('hexo-front-matter');
  4. const Promise = require('bluebird');
  5. const assignIn = (target, ...sources) => {
  6. const length = sources.length;
  7. if (length < 1 || target == null) return target;
  8. for (let i = 0; i < length; i++) {
  9. const source = sources[i];
  10. for (const key in source) {
  11. target[key] = source[key];
  12. }
  13. }
  14. return target;
  15. };
  16. class View {
  17. constructor(path, data) {
  18. this.path = path;
  19. this.source = join(this._theme.base, 'layout', path);
  20. this.data = typeof data === 'string' ? yfm(data) : data;
  21. this._precompile();
  22. }
  23. render(options = {}, callback) {
  24. if (!callback && typeof options === 'function') {
  25. callback = options;
  26. options = {};
  27. }
  28. const { data } = this;
  29. const { layout = options.layout } = data;
  30. const locals = this._buildLocals(options);
  31. return this._compiled(this._bindHelpers(locals)).then(result => {
  32. if (result == null || !layout) return result;
  33. const layoutView = this._resolveLayout(layout);
  34. if (!layoutView) return result;
  35. const layoutLocals = {
  36. ...locals,
  37. body: result,
  38. layout: false
  39. };
  40. return layoutView.render(layoutLocals, callback);
  41. }).asCallback(callback);
  42. }
  43. renderSync(options = {}) {
  44. const { data } = this;
  45. const { layout = options.layout } = data;
  46. const locals = this._buildLocals(options);
  47. const result = this._compiledSync(this._bindHelpers(locals));
  48. if (result == null || !layout) return result;
  49. const layoutView = this._resolveLayout(layout);
  50. if (!layoutView) return result;
  51. const layoutLocals = {
  52. ...locals,
  53. body: result,
  54. layout: false
  55. };
  56. return layoutView.renderSync(layoutLocals);
  57. }
  58. _buildLocals(locals) {
  59. // eslint-disable-next-line no-unused-vars
  60. const { layout, _content, ...data } = this.data;
  61. return assignIn({}, locals, data, {
  62. filename: this.source
  63. });
  64. }
  65. _bindHelpers(locals) {
  66. const helpers = this._helper.list();
  67. const keys = Object.keys(helpers);
  68. for (const key of keys) {
  69. locals[key] = helpers[key].bind(locals);
  70. }
  71. return locals;
  72. }
  73. _resolveLayout(name) {
  74. // Relative path
  75. const layoutPath = join(dirname(this.path), name);
  76. let layoutView = this._theme.getView(layoutPath);
  77. if (layoutView && layoutView.source !== this.source) return layoutView;
  78. // Absolute path
  79. layoutView = this._theme.getView(name);
  80. if (layoutView && layoutView.source !== this.source) return layoutView;
  81. }
  82. _precompile() {
  83. const render = this._render;
  84. const ctx = render.context;
  85. const ext = extname(this.path);
  86. const renderer = render.getRenderer(ext);
  87. const data = {
  88. path: this.source,
  89. text: this.data._content
  90. };
  91. function buildFilterArguments(result) {
  92. const output = render.getOutput(ext) || ext;
  93. return [
  94. `after_render:${output}`,
  95. result,
  96. {
  97. context: ctx,
  98. args: [data]
  99. }
  100. ];
  101. }
  102. if (renderer && typeof renderer.compile === 'function') {
  103. const compiled = renderer.compile(data);
  104. this._compiledSync = locals => {
  105. const result = compiled(locals);
  106. return ctx.execFilterSync(...buildFilterArguments(result));
  107. };
  108. this._compiled = locals => Promise.resolve(compiled(locals))
  109. .then(result => ctx.execFilter(...buildFilterArguments(result)));
  110. } else {
  111. this._compiledSync = locals => render.renderSync(data, locals);
  112. this._compiled = locals => render.render(data, locals);
  113. }
  114. }
  115. }
  116. module.exports = View;