diff --git a/lib/hexo/index.js b/lib/hexo/index.js index dc345a65d7..67c332ab28 100644 --- a/lib/hexo/index.js +++ b/lib/hexo/index.js @@ -253,15 +253,10 @@ class Hexo extends EventEmitter { args = {}; } - return new Promise((resolve, reject) => { - const c = this.extend.console.get(name); + const c = this.extend.console.get(name); - if (c) { - Reflect.apply(c, this, [args]).then(resolve, reject); - } else { - reject(new Error(`Console \`${name}\` has not been registered yet!`)); - } - }).asCallback(callback); + if (c) return Reflect.apply(c, this, [args]).asCallback(callback); + return Promise.reject(new Error(`Console \`${name}\` has not been registered yet!`)); } model(name, schema) { @@ -319,7 +314,8 @@ class Hexo extends EventEmitter { ]); }).then(() => { mergeCtxThemeConfig(this); - }).then(() => this._generate({cache: false})).asCallback(callback); + return this._generate({cache: false}); + }).asCallback(callback); } watch(callback) { @@ -344,7 +340,7 @@ class Hexo extends EventEmitter { ]); }).then(() => { mergeCtxThemeConfig(this); - }).then(() => { + this.source.on('processAfter', this._watchBox); this.theme.on('processAfter', () => { this._watchBox(); @@ -400,10 +396,8 @@ class Hexo extends EventEmitter { return Promise.map(Object.keys(generators), key => { const generator = generators[key]; - return Reflect.apply(generator, this, [siteLocals]).then(data => { - log.debug('Generator: %s', magenta(key)); - return data; - }); + log.debug('Generator: %s', magenta(key)); + return Reflect.apply(generator, this, [siteLocals]); }).reduce((result, data) => { return data ? result.concat(data) : result; }, []); @@ -432,10 +426,12 @@ class Hexo extends EventEmitter { .thenReturn(path); }).then(newRouteList => { // Remove old routes - const removed = routeList.filter(item => !newRouteList.includes(item)); + for (let i = 0, len = routeList.length; i < len; i++) { + const item = routeList[i]; - for (let i = 0, len = removed.length; i < len; i++) { - route.remove(removed[i]); + if (!newRouteList.includes(item)) { + route.remove(item); + } } }); } diff --git a/lib/hexo/load_plugins.js b/lib/hexo/load_plugins.js index 9b95c18ece..d48b8830cb 100644 --- a/lib/hexo/load_plugins.js +++ b/lib/hexo/load_plugins.js @@ -62,10 +62,6 @@ function loadModules(ctx) { function loadScripts(ctx) { const baseDirLength = ctx.base_dir.length; - function displayPath(path) { - return magenta(path.substring(baseDirLength)); - } - return Promise.filter([ ctx.theme_script_dir, ctx.script_dir @@ -75,9 +71,13 @@ function loadScripts(ctx) { const path = join(scriptDir, name); return ctx.loadPlugin(path).then(() => { - ctx.log.debug('Script loaded: %s', displayPath(path)); + ctx.log.debug('Script loaded: %s', displayPath(path, baseDirLength)); }).catch(err => { - ctx.log.error({err}, 'Script load failed: %s', displayPath(path)); + ctx.log.error({err}, 'Script load failed: %s', displayPath(path, baseDirLength)); }); })); } + +function displayPath(path, baseDirLength) { + return magenta(path.substring(baseDirLength)); +} diff --git a/lib/hexo/post.js b/lib/hexo/post.js index bc0214e826..77a7780312 100644 --- a/lib/hexo/post.js +++ b/lib/hexo/post.js @@ -16,15 +16,6 @@ const rHexoPostRenderEscape = /([\s\S]+?)<\/hexoPostRen const rSwigPlaceHolder = /(?:<|<)!--swig\uFFFC(\d+)--(?:>|>)/g; const rCodeBlockPlaceHolder = /(?:<|<)!--code\uFFFC(\d+)--(?:>|>)/g; -const _escapeContent = (cache, flag, str) => ``; - -const _restoreContent = cache => (_, index) => { - assert(cache[index]); - const value = cache[index]; - cache[index] = null; - return value; -}; - const STATE_PLAINTEXT = Symbol('plaintext'); const STATE_SWIG_VAR = Symbol('swig_var'); const STATE_SWIG_COMMENT = Symbol('swig_comment'); @@ -43,17 +34,30 @@ class PostRenderEscape { this.stored = []; } + static escapeContent(cache, flag, str) { + return ``; + } + + static restoreContent(cache) { + return (_, index) => { + assert(cache[index]); + const value = cache[index]; + cache[index] = null; + return value; + }; + } + restoreAllSwigTags(str) { - const restored = str.replace(rSwigPlaceHolder, _restoreContent(this.stored)); + const restored = str.replace(rSwigPlaceHolder, PostRenderEscape.restoreContent(this.stored)); return restored; } restoreCodeBlocks(str) { - return str.replace(rCodeBlockPlaceHolder, _restoreContent(this.stored)); + return str.replace(rCodeBlockPlaceHolder, PostRenderEscape.restoreContent(this.stored)); } escapeCodeBlocks(str) { - return str.replace(rHexoPostRenderEscape, (_, content) => _escapeContent(this.stored, 'code', content)); + return str.replace(rHexoPostRenderEscape, (_, content) => PostRenderEscape.escapeContent(this.stored, 'code', content)); } /** @@ -101,7 +105,7 @@ class PostRenderEscape { } else { swig_tag_name = ''; state = STATE_PLAINTEXT; - output += _escapeContent(this.stored, 'swig', `{%${buffer}%}`); + output += PostRenderEscape.escapeContent(this.stored, 'swig', `{%${buffer}%}`); } buffer = ''; @@ -127,7 +131,7 @@ class PostRenderEscape { if (char === '}' && next_char === '}') { idx++; state = STATE_PLAINTEXT; - output += _escapeContent(this.stored, 'swig', `{{${buffer}}}`); + output += PostRenderEscape.escapeContent(this.stored, 'swig', `{{${buffer}}}`); buffer = ''; } else { buffer = buffer + char; @@ -157,7 +161,7 @@ class PostRenderEscape { if (swig_full_tag_end_buffer.includes(`end${swig_tag_name}`)) { state = STATE_PLAINTEXT; - output += _escapeContent(this.stored, 'swig', `{%${swig_full_tag_start_buffer}%}${buffer}{%${swig_full_tag_end_buffer}%}`); + output += PostRenderEscape.escapeContent(this.stored, 'swig', `{%${swig_full_tag_start_buffer}%}${buffer}{%${swig_full_tag_end_buffer}%}`); idx = _idx; swig_full_tag_start_buffer = ''; swig_full_tag_end_buffer = ''; @@ -241,7 +245,8 @@ class Post { createAssetFolder(path, config.post_asset_folder) ]).then(() => { ctx.emit('new', result); - }).thenReturn(result); + return result; + }); }).asCallback(callback); } @@ -271,12 +276,11 @@ class Post { // Parse front-matter const obj = jsonMode ? JSON.parse(`{${frontMatter}}`) : load(frontMatter); - // Add data which are not in the front-matter - for (const key of Object.keys(data)) { - if (!preservedKeys.includes(key) && obj[key] == null) { + Object.keys(data) + .filter(key => !preservedKeys.includes(key) && obj[key] == null) + .forEach(key => { obj[key] = data[key]; - } - } + }); let content = ''; // Prepend the separator diff --git a/lib/hexo/render.js b/lib/hexo/render.js index 693cfe90dd..802d1b38f0 100644 --- a/lib/hexo/render.js +++ b/lib/hexo/render.js @@ -60,13 +60,19 @@ class Render { const ctx = this.context; let ext = ''; - return new Promise((resolve, reject) => { - if (!data) return reject(new TypeError('No input file or string!')); - if (data.text != null) return resolve(data.text); - if (!data.path) return reject(new TypeError('No input file or string!')); + let promise; - readFile(data.path).then(resolve, reject); - }).then(text => { + if (!data) return Promise.reject(new TypeError('No input file or string!')); + + if (data.text != null) { + promise = Promise.resolve(data.text); + } else if (!data.path) { + return Promise.reject(new TypeError('No input file or string!')); + } else { + promise = readFile(data.path); + } + + return promise.then(text => { data.text = text; ext = data.engine || getExtname(data.path); if (!ext || !this.isRenderable(ext)) return text; diff --git a/lib/plugins/processor/asset.js b/lib/plugins/processor/asset.js index b8e7ffc828..4131294a30 100644 --- a/lib/plugins/processor/asset.js +++ b/lib/plugins/processor/asset.js @@ -7,124 +7,124 @@ const { extname, relative } = require('path'); const { Pattern } = require('hexo-util'); module.exports = ctx => { - function processPage(file) { - const Page = ctx.model('Page'); - const { path } = file; - const doc = Page.findOne({source: path}); - const { config } = ctx; - const { timezone: timezoneCfg } = config; - // Deprecated: use_date_for_updated will be removed in future - const updated_option = config.use_date_for_updated === true ? 'date' : config.updated_option; + return { + pattern: new Pattern(path => { + if (isExcludedFile(path, ctx.config)) return; - if (file.type === 'skip' && doc) { - return; - } + return { + renderable: ctx.render.isRenderable(path) && !isMatch(path, ctx.config.skip_render) + }; + }), - if (file.type === 'delete') { - if (doc) { - return doc.remove(); + process: function assetProcessor(file) { + if (file.params.renderable) { + return processPage(ctx, file); } - return; + return processAsset(ctx, file); } + }; +}; - return Promise.all([ - file.stat(), - file.read() - ]).spread((stats, content) => { - const data = yfm(content); - const output = ctx.render.getOutput(path); - - data.source = path; - data.raw = content; +function processPage(ctx, file) { + const Page = ctx.model('Page'); + const { path } = file; + const doc = Page.findOne({source: path}); + const { config } = ctx; + const { timezone: timezoneCfg } = config; + // Deprecated: use_date_for_updated will be removed in future + const updated_option = config.use_date_for_updated === true ? 'date' : config.updated_option; + + if (file.type === 'skip' && doc) { + return; + } - data.date = toDate(data.date); + if (file.type === 'delete') { + if (doc) { + return doc.remove(); + } - if (data.date) { - if (timezoneCfg) data.date = timezone(data.date, timezoneCfg); - } else { - data.date = stats.ctime; - } + return; + } - data.updated = toDate(data.updated); + return Promise.all([ + file.stat(), + file.read() + ]).spread((stats, content) => { + const data = yfm(content); + const output = ctx.render.getOutput(path); - if (data.updated) { - if (timezoneCfg) data.updated = timezone(data.updated, timezoneCfg); - } else if (updated_option === 'date') { - data.updated = data.date; - } else if (updated_option === 'empty') { - data.updated = undefined; - } else { - data.updated = stats.mtime; - } + data.source = path; + data.raw = content; - if (data.permalink) { - data.path = data.permalink; - data.permalink = undefined; + data.date = toDate(data.date); - if (data.path.endsWith('/')) { - data.path += 'index'; - } + if (data.date) { + if (timezoneCfg) data.date = timezone(data.date, timezoneCfg); + } else { + data.date = stats.ctime; + } - if (!extname(data.path)) { - data.path += `.${output}`; - } - } else { - data.path = `${path.substring(0, path.length - extname(path).length)}.${output}`; - } + data.updated = toDate(data.updated); - if (!data.layout && output !== 'html' && output !== 'htm') { - data.layout = false; - } + if (data.updated) { + if (timezoneCfg) data.updated = timezone(data.updated, timezoneCfg); + } else if (updated_option === 'date') { + data.updated = data.date; + } else if (updated_option === 'empty') { + data.updated = undefined; + } else { + data.updated = stats.mtime; + } - // FIXME: Data may be inserted when reading files. Load it again to prevent - // race condition. We have to solve this in warehouse. - const doc = Page.findOne({source: path}); + if (data.permalink) { + data.path = data.permalink; + data.permalink = undefined; - if (doc) { - return doc.replace(data); + if (data.path.endsWith('/')) { + data.path += 'index'; } - return Page.insert(data); - }); - } - - function processAsset(file) { - const id = relative(ctx.base_dir, file.source).replace(/\\/g, '/'); - const Asset = ctx.model('Asset'); - const doc = Asset.findById(id); - - if (file.type === 'delete') { - if (doc) { - return doc.remove(); + if (!extname(data.path)) { + data.path += `.${output}`; } + } else { + data.path = `${path.substring(0, path.length - extname(path).length)}.${output}`; + } - return; + if (!data.layout && output !== 'html' && output !== 'htm') { + data.layout = false; } - return Asset.save({ - _id: id, - path: file.path, - modified: file.type !== 'skip', - renderable: file.params.renderable - }); - } + // FIXME: Data may be inserted when reading files. Load it again to prevent + // race condition. We have to solve this in warehouse. + const doc = Page.findOne({source: path}); - return { - pattern: new Pattern(path => { - if (isExcludedFile(path, ctx.config)) return; + if (doc) { + return doc.replace(data); + } - return { - renderable: ctx.render.isRenderable(path) && !isMatch(path, ctx.config.skip_render) - }; - }), + return Page.insert(data); + }); +} - process: function assetProcessor(file) { - if (file.params.renderable) { - return processPage(file); - } +function processAsset(ctx, file) { + const id = relative(ctx.base_dir, file.source).replace(/\\/g, '/'); + const Asset = ctx.model('Asset'); + const doc = Asset.findById(id); - return processAsset(file); + if (file.type === 'delete') { + if (doc) { + return doc.remove(); } - }; -}; + + return; + } + + return Asset.save({ + _id: id, + path: file.path, + modified: file.type !== 'skip', + renderable: file.params.renderable + }); +} diff --git a/lib/plugins/processor/post.js b/lib/plugins/processor/post.js index f5fba69ef5..9b4272926a 100644 --- a/lib/plugins/processor/post.js +++ b/lib/plugins/processor/post.js @@ -22,205 +22,6 @@ const preservedKeys = { }; module.exports = ctx => { - function processPost(file) { - const Post = ctx.model('Post'); - const { path } = file.params; - const doc = Post.findOne({source: file.path}); - const { config } = ctx; - const { timezone: timezoneCfg } = config; - // Deprecated: use_date_for_updated will be removed in future - const updated_option = config.use_date_for_updated === true ? 'date' : config.updated_option; - let categories, tags; - - if (file.type === 'skip' && doc) { - return; - } - - if (file.type === 'delete') { - if (doc) { - return doc.remove(); - } - - return; - } - - return Promise.all([ - file.stat(), - file.read() - ]).spread((stats, content) => { - const data = yfm(content); - const info = parseFilename(config.new_post_name, path); - const keys = Object.keys(info); - - data.source = file.path; - data.raw = content; - data.slug = info.title; - - if (file.params.published) { - if (!Object.prototype.hasOwnProperty.call(data, 'published')) data.published = true; - } else { - data.published = false; - } - - for (let i = 0, len = keys.length; i < len; i++) { - const key = keys[i]; - if (!preservedKeys[key]) data[key] = info[key]; - } - - if (data.date) { - data.date = toDate(data.date); - } else if (info && info.year && (info.month || info.i_month) && (info.day || info.i_day)) { - data.date = new Date( - info.year, - parseInt(info.month || info.i_month, 10) - 1, - parseInt(info.day || info.i_day, 10) - ); - } - - if (data.date) { - if (timezoneCfg) data.date = timezone(data.date, timezoneCfg); - } else { - data.date = stats.birthtime; - } - - data.updated = toDate(data.updated); - - if (data.updated) { - if (timezoneCfg) data.updated = timezone(data.updated, timezoneCfg); - } else if (updated_option === 'date') { - data.updated = data.date; - } else if (updated_option === 'empty') { - data.updated = undefined; - } else { - data.updated = stats.mtime; - } - - if (data.category && !data.categories) { - data.categories = data.category; - data.category = undefined; - } - - if (data.tag && !data.tags) { - data.tags = data.tag; - data.tag = undefined; - } - - categories = data.categories || []; - tags = data.tags || []; - - if (!Array.isArray(categories)) categories = [categories]; - if (!Array.isArray(tags)) tags = [tags]; - - if (data.photo && !data.photos) { - data.photos = data.photo; - data.photo = undefined; - } - - if (data.photos && !Array.isArray(data.photos)) { - data.photos = [data.photos]; - } - - if (data.link && !data.title) { - data.title = data.link.replace(/^https?:\/\/|\/$/g, ''); - } - - if (data.permalink) { - data.__permalink = data.permalink; - data.permalink = undefined; - } - - // FIXME: Data may be inserted when reading files. Load it again to prevent - // race condition. We have to solve this in warehouse. - const doc = Post.findOne({source: file.path}); - - if (doc) { - return doc.replace(data); - } - - return Post.insert(data); - }).then(doc => Promise.all([ - doc.setCategories(categories), - doc.setTags(tags), - scanAssetDir(doc) - ])); - } - - function shouldSkipAsset(post, asset) { - if (!ctx._showDrafts()) { - if (post.published === false && asset) { - // delete existing draft assets if draft posts are hidden - asset.remove(); - } - if (post.published === false) { - // skip draft assets if draft posts are hidden - return true; - } - } - - return asset !== undefined; // skip already existing assets - } - - function scanAssetDir(post) { - if (!ctx.config.post_asset_folder) return; - - const assetDir = post.asset_dir; - const baseDir = ctx.base_dir; - const baseDirLength = baseDir.length; - const PostAsset = ctx.model('PostAsset'); - - return stat(assetDir).then(stats => { - if (!stats.isDirectory()) return []; - - return listDir(assetDir); - }).catch(err => { - if (err && err.code === 'ENOENT') return []; - throw err; - }).filter(item => !isExcludedFile(item, ctx.config)).map(item => { - const id = join(assetDir, item).substring(baseDirLength).replace(/\\/g, '/'); - const asset = PostAsset.findById(id); - - if (shouldSkipAsset(post, asset)) return undefined; - - return PostAsset.save({ - _id: id, - post: post._id, - slug: item, - modified: true - }); - }); - } - - function processAsset(file) { - const PostAsset = ctx.model('PostAsset'); - const Post = ctx.model('Post'); - const id = file.source.substring(ctx.base_dir.length).replace(/\\/g, '/'); - const doc = PostAsset.findById(id); - - if (file.type === 'delete') { - if (doc) { - return doc.remove(); - } - - return; - } - - // TODO: Better post searching - const post = Post.toArray().find(post => file.source.startsWith(post.asset_dir)); - if (post != null && (post.published || ctx._showDrafts())) { - return PostAsset.save({ - _id: id, - slug: file.source.substring(post.asset_dir.length), - post: post._id, - modified: file.type !== 'skip', - renderable: file.params.renderable - }); - } - - if (doc) { - return doc.remove(); - } - } - return { pattern: new Pattern(path => { if (isTmpFile(path)) return; @@ -247,14 +48,137 @@ module.exports = ctx => { process: function postProcessor(file) { if (file.params.renderable) { - return processPost(file); + return processPost(ctx, file); } else if (ctx.config.post_asset_folder) { - return processAsset(file); + return processAsset(ctx, file); } } }; }; +function processPost(ctx, file) { + const Post = ctx.model('Post'); + const { path } = file.params; + const doc = Post.findOne({source: file.path}); + const { config } = ctx; + const { timezone: timezoneCfg } = config; + // Deprecated: use_date_for_updated will be removed in future + const updated_option = config.use_date_for_updated === true ? 'date' : config.updated_option; + let categories, tags; + + if (file.type === 'skip' && doc) { + return; + } + + if (file.type === 'delete') { + if (doc) { + return doc.remove(); + } + + return; + } + + return Promise.all([ + file.stat(), + file.read() + ]).spread((stats, content) => { + const data = yfm(content); + const info = parseFilename(config.new_post_name, path); + const keys = Object.keys(info); + + data.source = file.path; + data.raw = content; + data.slug = info.title; + + if (file.params.published) { + if (!Object.prototype.hasOwnProperty.call(data, 'published')) data.published = true; + } else { + data.published = false; + } + + for (let i = 0, len = keys.length; i < len; i++) { + const key = keys[i]; + if (!preservedKeys[key]) data[key] = info[key]; + } + + if (data.date) { + data.date = toDate(data.date); + } else if (info && info.year && (info.month || info.i_month) && (info.day || info.i_day)) { + data.date = new Date( + info.year, + parseInt(info.month || info.i_month, 10) - 1, + parseInt(info.day || info.i_day, 10) + ); + } + + if (data.date) { + if (timezoneCfg) data.date = timezone(data.date, timezoneCfg); + } else { + data.date = stats.birthtime; + } + + data.updated = toDate(data.updated); + + if (data.updated) { + if (timezoneCfg) data.updated = timezone(data.updated, timezoneCfg); + } else if (updated_option === 'date') { + data.updated = data.date; + } else if (updated_option === 'empty') { + data.updated = undefined; + } else { + data.updated = stats.mtime; + } + + if (data.category && !data.categories) { + data.categories = data.category; + data.category = undefined; + } + + if (data.tag && !data.tags) { + data.tags = data.tag; + data.tag = undefined; + } + + categories = data.categories || []; + tags = data.tags || []; + + if (!Array.isArray(categories)) categories = [categories]; + if (!Array.isArray(tags)) tags = [tags]; + + if (data.photo && !data.photos) { + data.photos = data.photo; + data.photo = undefined; + } + + if (data.photos && !Array.isArray(data.photos)) { + data.photos = [data.photos]; + } + + if (data.link && !data.title) { + data.title = data.link.replace(/^https?:\/\/|\/$/g, ''); + } + + if (data.permalink) { + data.__permalink = data.permalink; + data.permalink = undefined; + } + + // FIXME: Data may be inserted when reading files. Load it again to prevent + // race condition. We have to solve this in warehouse. + const doc = Post.findOne({source: file.path}); + + if (doc) { + return doc.replace(data); + } + + return Post.insert(data); + }).then(doc => Promise.all([ + doc.setCategories(categories), + doc.setTags(tags), + scanAssetDir(ctx, doc) + ])); +} + function parseFilename(config, path) { config = config.substring(0, config.length - extname(config).length); path = path.substring(0, path.length - extname(path).length); @@ -282,3 +206,79 @@ function parseFilename(config, path) { title: slugize(path) }; } + +function scanAssetDir(ctx, post) { + if (!ctx.config.post_asset_folder) return; + + const assetDir = post.asset_dir; + const baseDir = ctx.base_dir; + const baseDirLength = baseDir.length; + const PostAsset = ctx.model('PostAsset'); + + return stat(assetDir).then(stats => { + if (!stats.isDirectory()) return []; + + return listDir(assetDir); + }).catch(err => { + if (err && err.code === 'ENOENT') return []; + throw err; + }).filter(item => !isExcludedFile(item, ctx.config)).map(item => { + const id = join(assetDir, item).substring(baseDirLength).replace(/\\/g, '/'); + const asset = PostAsset.findById(id); + + if (shouldSkipAsset(ctx, post, asset)) return undefined; + + return PostAsset.save({ + _id: id, + post: post._id, + slug: item, + modified: true + }); + }); +} + +function shouldSkipAsset(ctx, post, asset) { + if (!ctx._showDrafts()) { + if (post.published === false && asset) { + // delete existing draft assets if draft posts are hidden + asset.remove(); + } + if (post.published === false) { + // skip draft assets if draft posts are hidden + return true; + } + } + + return asset !== undefined; // skip already existing assets +} + +function processAsset(ctx, file) { + const PostAsset = ctx.model('PostAsset'); + const Post = ctx.model('Post'); + const id = file.source.substring(ctx.base_dir.length).replace(/\\/g, '/'); + const doc = PostAsset.findById(id); + + if (file.type === 'delete') { + if (doc) { + return doc.remove(); + } + + return; + } + + // TODO: Better post searching + const post = Post.toArray().find(post => file.source.startsWith(post.asset_dir)); + if (post != null && (post.published || ctx._showDrafts())) { + return PostAsset.save({ + _id: id, + slug: file.source.substring(post.asset_dir.length), + post: post._id, + modified: file.type !== 'skip', + renderable: file.params.renderable + }); + } + + if (doc) { + return doc.remove(); + } +}