diff --git a/lib/hexo/post.js b/lib/hexo/post.js index 7f0db61bbe..8d5f98c542 100644 --- a/lib/hexo/post.js +++ b/lib/hexo/post.js @@ -306,6 +306,59 @@ class Post { }); } + unpublish(data, replace, callback) { + if (!callback && typeof replace === 'function') { + callback = replace; + replace = false; + } + + if (data.layout === 'post') data.layout = 'draft'; + + const ctx = this.context; + const { config } = ctx; + const postDir = join(ctx.source_dir, '_posts'); + const slug = slugize(data.slug.toString(), { transform: config.filename_case }); + data.slug = slug; + const regex = new RegExp(`^${escapeRegExp(slug)}(?:[^\\/\\\\]+)`); + let src = ''; + const result = {}; + + data.layout = (data.layout || config.default_layout).toLowerCase(); + + // Find the original post + return listDir(postDir).then(list => { + const item = list.find(item => regex.test(item)); + if (!item) throw new Error(`Post "${slug}" does not exist.`); + + // Read the post content + src = join(postDir, item); + return readFile(src); + }).then(content => { + // Create draft + Object.assign(data, yfmParse(content)); + data.content = data._content; + data._content = undefined; + + return this.create(data, replace); + }).then(post => { + result.path = post.path; + result.content = post.content; + return unlink(src); + }).then(() => { // Remove the original post file + if (!config.post_asset_folder) return; + + // Copy assets + const assetSrc = removeExtname(src); + const assetDest = removeExtname(result.path); + + return exists(assetSrc).then(exist => { + if (!exist) return; + + return copyDir(assetSrc, assetDest).then(() => rmdir(assetSrc)); + }); + }).thenReturn(result).asCallback(callback); + } + publish(data, replace, callback) { if (!callback && typeof replace === 'function') { callback = replace; diff --git a/lib/plugins/console/index.js b/lib/plugins/console/index.js index 3a34b7b0fb..40960cacd0 100644 --- a/lib/plugins/console/index.js +++ b/lib/plugins/console/index.js @@ -67,6 +67,14 @@ module.exports = function(ctx) { ] }, require('./publish')); + console.register('unpublish', 'Moves a published post from _posts to _drafts folder.', { + usage: '[layout] ', + arguments: [ + {name: 'layout', desc: 'Post layout. Use post, page, draft or whatever you want.'}, + {name: 'filename', desc: 'Post filename. "hello-world" for example.'} + ] + }, require('./unpublish')); + console.register('render', 'Render files with renderer plugins.', { init: true, desc: 'Render files with renderer plugins (e.g. Markdown) and save them at the specified path.', diff --git a/lib/plugins/console/unpublish.js b/lib/plugins/console/unpublish.js new file mode 100644 index 0000000000..4cb8fd8c65 --- /dev/null +++ b/lib/plugins/console/unpublish.js @@ -0,0 +1,20 @@ +'use strict'; + +const tildify = require('tildify'); +const { magenta } = require('picocolors'); + +function unPublishConsole(args) { + // Display help message if user didn't input any arguments + if (!args._.length) { + return this.call('help', {_: ['unpublish']}); + } + + return this.post.unpublish({ + slug: args._.pop(), + layout: 'draft' // assumes only published posts to be reverteds + }, args.r || args.replace).then(post => { + this.log.info('Unpublished: %s', magenta(tildify(post.path))); + }); +} + +module.exports = unPublishConsole; diff --git a/test/scripts/console/index.js b/test/scripts/console/index.js index 2c9a1d2208..c638a24bf8 100644 --- a/test/scripts/console/index.js +++ b/test/scripts/console/index.js @@ -8,6 +8,7 @@ describe('Console', () => { require('./migrate'); require('./new'); require('./publish'); + require('./unpublish'); require('./render'); require('./list'); require('./list_post'); diff --git a/test/scripts/console/unpublish.js b/test/scripts/console/unpublish.js new file mode 100644 index 0000000000..7145edbf7a --- /dev/null +++ b/test/scripts/console/unpublish.js @@ -0,0 +1,152 @@ +'use strict'; + +const { exists, mkdirs, readFile, rmdir, unlink } = require('hexo-fs'); +const moment = require('moment'); +const { join } = require('path'); +const Promise = require('bluebird'); +const { useFakeTimers, spy } = require('sinon'); + +describe('unpublish', () => { + const Hexo = require('../../../lib/hexo'); + const hexo = new Hexo(join(__dirname, 'unpublish_test'), { silent: true }); + const unpublish = require('../../../lib/plugins/console/unpublish').bind( + hexo + ); + const post = hexo.post; + const now = Date.now(); + let clock; + + before(async () => { + clock = useFakeTimers(now); + + await mkdirs(hexo.base_dir); + await hexo.init(); + await hexo.scaffold.set( + 'post', + ['---', 'title: {{ title }}', 'date: {{ date }}', 'tags:', '---'].join( + '\n' + ) + ); + await hexo.scaffold.set( + 'draft', + ['---', 'title: {{ title }}', 'tags:', '---'].join('\n') + ); + }); + + after(() => { + clock.restore(); + return rmdir(hexo.base_dir); + }); + + beforeEach(() => + post.create({ + title: 'Hello World', + layout: 'post', + date: moment(now).format('YYYY-MM-DD HH:mm:ss') + }) + ); + + it('slug', async () => { + const postPath = join(hexo.source_dir, '_posts', 'Hello-World.md'); + const path = join(hexo.source_dir, '_drafts', 'Hello-World.md'); + + const content + = ['---', 'title: Hello World', 'tags:', '---'].join('\n') + '\n'; + + await unpublish({ + _: ['Hello-World'] + }); + + const exist = await exists(postPath); + const data = await readFile(path); + + exist.should.be.false; + data.should.eql(content); + + await unlink(path); + }); + + it('no args', async () => { + const hexo = new Hexo(join(__dirname, 'unpublish_test'), { silent: true }); + hexo.call = spy(); + const unpublish = require('../../../lib/plugins/console/unpublish').bind( + hexo + ); + + await unpublish({ _: [] }); + + hexo.call.calledOnce.should.be.true; + hexo.call.args[0][0].should.eql('help'); + hexo.call.args[0][1]._[0].should.eql('unpublish'); + }); + + it('layout', async () => { + const path = join(hexo.source_dir, '_posts', 'Hello-World.md'); + const date = moment(now); + + const content + = [ + '---', + 'title: Hello World', + 'date: ' + date.format('YYYY-MM-DD HH:mm:ss'), + 'tags:', + '---' + ].join('\n') + '\n'; + + await unpublish({ + _: ['photo', 'Hello-World'] + }); + const data = await readFile(path); + data.should.eql(content); + + await unlink(path); + }); + + it('rename if target existed', async () => { + const path = join(hexo.source_dir, '_drafts', 'Hello-World-1.md'); + const publish = require('../../../lib/plugins/console/publish').bind(hexo); + + await post.create({ + title: 'Hello World', + layout: 'draft' + }); + await publish({ + _: ['Hello-World'] + }); + await unpublish({ + _: ['Hello-World'] + }); + + const exist = await exists(path); + exist.should.be.true; + + await Promise.all([ + unlink(path), + unlink(join(hexo.source_dir, '_drafts', 'Hello-World.md')) + ]); + }); + + it('replace existing target', async () => { + const path = join(hexo.source_dir, '_drafts', 'Hello-World.md'); + const publish = require('../../../lib/plugins/console/publish').bind(hexo); + + await post.create({ + title: 'Hello World', + layout: 'draft' + }); + await publish({ + _: ['Hello-World'] + }); + await unpublish({ + _: ['Hello-World'], + replace: true + }); + + const exist = await exists( + join(hexo.source_dir, '_drafts', 'Hello-World-1.md') + ); + exist.should.be.false; + + await unlink(path); + }); +});