post.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. 'use strict';
  2. const { toDate, timezone, isExcludedFile, isTmpFile, isHiddenFile, isMatch } = require('./common');
  3. const Promise = require('bluebird');
  4. const { parse: yfm } = require('hexo-front-matter');
  5. const { extname, join } = require('path');
  6. const { stat, listDir } = require('hexo-fs');
  7. const { slugize, Pattern, Permalink } = require('hexo-util');
  8. const postDir = '_posts/';
  9. const draftDir = '_drafts/';
  10. let permalink;
  11. const preservedKeys = {
  12. title: true,
  13. year: true,
  14. month: true,
  15. day: true,
  16. i_month: true,
  17. i_day: true,
  18. hash: true
  19. };
  20. module.exports = ctx => {
  21. function processPost(file) {
  22. const Post = ctx.model('Post');
  23. const { path } = file.params;
  24. const doc = Post.findOne({source: file.path});
  25. const { config } = ctx;
  26. const { timezone: timezoneCfg } = config;
  27. // Deprecated: use_date_for_updated will be removed in future
  28. const updated_option = config.use_date_for_updated === true ? 'date' : config.updated_option;
  29. let categories, tags;
  30. if (file.type === 'skip' && doc) {
  31. return;
  32. }
  33. if (file.type === 'delete') {
  34. if (doc) {
  35. return doc.remove();
  36. }
  37. return;
  38. }
  39. return Promise.all([
  40. file.stat(),
  41. file.read()
  42. ]).spread((stats, content) => {
  43. const data = yfm(content);
  44. const info = parseFilename(config.new_post_name, path);
  45. const keys = Object.keys(info);
  46. data.source = file.path;
  47. data.raw = content;
  48. data.slug = info.title;
  49. if (file.params.published) {
  50. if (!Object.prototype.hasOwnProperty.call(data, 'published')) data.published = true;
  51. } else {
  52. data.published = false;
  53. }
  54. for (let i = 0, len = keys.length; i < len; i++) {
  55. const key = keys[i];
  56. if (!preservedKeys[key]) data[key] = info[key];
  57. }
  58. if (data.date) {
  59. data.date = toDate(data.date);
  60. } else if (info && info.year && (info.month || info.i_month) && (info.day || info.i_day)) {
  61. data.date = new Date(
  62. info.year,
  63. parseInt(info.month || info.i_month, 10) - 1,
  64. parseInt(info.day || info.i_day, 10)
  65. );
  66. }
  67. if (data.date) {
  68. if (timezoneCfg) data.date = timezone(data.date, timezoneCfg);
  69. } else {
  70. data.date = stats.birthtime;
  71. }
  72. data.updated = toDate(data.updated);
  73. if (data.updated) {
  74. if (timezoneCfg) data.updated = timezone(data.updated, timezoneCfg);
  75. } else if (updated_option === 'date') {
  76. data.updated = data.date;
  77. } else if (updated_option === 'empty') {
  78. delete data.updated;
  79. } else {
  80. data.updated = stats.mtime;
  81. }
  82. if (data.category && !data.categories) {
  83. data.categories = data.category;
  84. delete data.category;
  85. }
  86. if (data.tag && !data.tags) {
  87. data.tags = data.tag;
  88. delete data.tag;
  89. }
  90. categories = data.categories || [];
  91. tags = data.tags || [];
  92. if (!Array.isArray(categories)) categories = [categories];
  93. if (!Array.isArray(tags)) tags = [tags];
  94. if (data.photo && !data.photos) {
  95. data.photos = data.photo;
  96. delete data.photo;
  97. }
  98. if (data.photos && !Array.isArray(data.photos)) {
  99. data.photos = [data.photos];
  100. }
  101. if (data.link && !data.title) {
  102. data.title = data.link.replace(/^https?:\/\/|\/$/g, '');
  103. }
  104. if (data.permalink) {
  105. data.__permalink = data.permalink;
  106. delete data.permalink;
  107. }
  108. // FIXME: Data may be inserted when reading files. Load it again to prevent
  109. // race condition. We have to solve this in warehouse.
  110. const doc = Post.findOne({source: file.path});
  111. if (doc) {
  112. return doc.replace(data);
  113. }
  114. return Post.insert(data);
  115. }).then(doc => Promise.all([
  116. doc.setCategories(categories),
  117. doc.setTags(tags),
  118. scanAssetDir(doc)
  119. ]));
  120. }
  121. function scanAssetDir(post) {
  122. if (!ctx.config.post_asset_folder) return;
  123. const assetDir = post.asset_dir;
  124. const baseDir = ctx.base_dir;
  125. const baseDirLength = baseDir.length;
  126. const PostAsset = ctx.model('PostAsset');
  127. return stat(assetDir).then(stats => {
  128. if (!stats.isDirectory()) return [];
  129. return listDir(assetDir);
  130. }).catch(err => {
  131. if (err && err.code === 'ENOENT') return [];
  132. throw err;
  133. }).filter(item => !isExcludedFile(item, ctx.config)).map(item => {
  134. const id = join(assetDir, item).substring(baseDirLength).replace(/\\/g, '/');
  135. const asset = PostAsset.findById(id);
  136. if (asset) return post.published === false ? asset.remove() : undefined; // delete if already exist
  137. else if (post.published === false) return undefined; // skip assets for unpulished posts and
  138. return PostAsset.save({
  139. _id: id,
  140. post: post._id,
  141. slug: item,
  142. modified: true
  143. });
  144. });
  145. }
  146. function processAsset(file) {
  147. const PostAsset = ctx.model('PostAsset');
  148. const Post = ctx.model('Post');
  149. const id = file.source.substring(ctx.base_dir.length).replace(/\\/g, '/');
  150. const doc = PostAsset.findById(id);
  151. if (file.type === 'delete') {
  152. if (doc) {
  153. return doc.remove();
  154. }
  155. return;
  156. }
  157. // TODO: Better post searching
  158. const post = Post.toArray().find(post => file.source.startsWith(post.asset_dir));
  159. if (post != null && post.published) {
  160. return PostAsset.save({
  161. _id: id,
  162. slug: file.source.substring(post.asset_dir.length),
  163. post: post._id,
  164. modified: file.type !== 'skip',
  165. renderable: file.params.renderable
  166. });
  167. }
  168. if (doc) {
  169. return doc.remove();
  170. }
  171. }
  172. return {
  173. pattern: new Pattern(path => {
  174. if (isTmpFile(path)) return;
  175. let result;
  176. if (path.startsWith(postDir)) {
  177. result = {
  178. published: true,
  179. path: path.substring(postDir.length)
  180. };
  181. } else if (path.startsWith(draftDir)) {
  182. result = {
  183. published: false,
  184. path: path.substring(draftDir.length)
  185. };
  186. }
  187. if (!result || isHiddenFile(result.path)) return;
  188. result.renderable = ctx.render.isRenderable(path) && !isMatch(path, ctx.config.skip_render);
  189. return result;
  190. }),
  191. process: function postProcessor(file) {
  192. if (file.params.renderable) {
  193. return processPost(file);
  194. } else if (ctx.config.post_asset_folder) {
  195. return processAsset(file);
  196. }
  197. }
  198. };
  199. };
  200. function parseFilename(config, path) {
  201. config = config.substring(0, config.length - extname(config).length);
  202. path = path.substring(0, path.length - extname(path).length);
  203. if (!permalink || permalink.rule !== config) {
  204. permalink = new Permalink(config, {
  205. segments: {
  206. year: /(\d{4})/,
  207. month: /(\d{2})/,
  208. day: /(\d{2})/,
  209. i_month: /(\d{1,2})/,
  210. i_day: /(\d{1,2})/,
  211. hash: /([0-9a-f]{12})/
  212. }
  213. });
  214. }
  215. const data = permalink.parse(path);
  216. if (data) {
  217. return data;
  218. }
  219. return {
  220. title: slugize(path)
  221. };
  222. }