Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 44 additions & 7 deletions lib/hexo/post.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import assert from 'assert';
import moment from 'moment';
import Promise from 'bluebird';
import { join, extname, basename } from 'path';
import { join, extname, basename, posix } from 'path';
import { magenta } from 'picocolors';
import { load } from 'js-yaml';
import { slugize, escapeRegExp, deepMerge} from 'hexo-util';
import { slugize, deepMerge} from 'hexo-util';
import { copyDir, exists, listDir, mkdirs, readFile, rmdir, unlink, writeFile } from 'hexo-fs';
import { parse as yfmParse, split as yfmSplit, stringify as yfmStringify } from 'hexo-front-matter';
import type Hexo from './index';
Expand Down Expand Up @@ -360,6 +360,44 @@ const removeExtname = (str: string) => {
return str.substring(0, str.length - extname(str).length);
};

const normalizeSourcePath = (str: string) => str.replace(/\\/g, '/').replace(/^(?:\.\/)+/, '');

const removeSourceExtname = (str: string) => {
return str.substring(0, str.length - posix.extname(str).length);
};

const normalizeSourceBasename = (str: string, filenameCase: number) => {
const extension = posix.extname(str);
const directory = posix.dirname(str);
const filename = posix.basename(str, extension);
const normalizedFilename = slugize(filename, { transform: filenameCase }) + extension;

return directory === '.' ? normalizedFilename : posix.join(directory, normalizedFilename);
};

const findDraftFile = (list: string[], value: string | number, filenameCase: number) => {
const source = normalizeSourcePath(value.toString());
const normalizedSource = normalizeSourceBasename(source, filenameCase);
const files = list.map(item => ({ item, source: normalizeSourcePath(item) }));
const candidates = source === normalizedSource ? [source] : [source, normalizedSource];

for (const candidate of candidates) {
const exactMatch = files.find(file => file.source === candidate);
if (exactMatch) return exactMatch.item;

const matches = files.filter(file => removeSourceExtname(file.source) === candidate);

if (matches.length === 1) return matches[0].item;

if (matches.length > 1) {
const filenames = matches.map(file => file.source).sort().join(', ');
throw new Error(`Draft "${source}" is ambiguous. Please specify the full filename: ${filenames}.`);
}
}

throw new Error(`Draft "${source}" does not exist.`);
};

const createAssetFolder = (path: string, assetFolder: boolean) => {
if (!assetFolder) return Promise.resolve();

Expand Down Expand Up @@ -491,18 +529,17 @@ class Post {
const ctx = this.context;
const { config } = ctx;
const draftDir = join(ctx.source_dir, '_drafts');
const slug = slugize(data.slug.toString(), { transform: config.filename_case });
data.slug = slug;
const regex = new RegExp(`^${escapeRegExp(slug)}(?:[^\\/\\\\]+)`);
const source = data.slug;
let src = '';
const result: Result = {} as any;

data.layout = (data.layout || config.default_layout).toLowerCase();

// Find the draft
return listDir(draftDir).then(list => {
const item = list.find(item => regex.test(item));
if (!item) throw new Error(`Draft "${slug}" does not exist.`);
const item = findDraftFile(list, source, config.filename_case);
const sourcePath = normalizeSourcePath(item);
data.slug = slugize(posix.basename(removeSourceExtname(sourcePath)), { transform: config.filename_case });

// Read the content
src = join(draftDir, item);
Expand Down
2 changes: 1 addition & 1 deletion lib/plugins/console/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export = function(ctx: Hexo) {
usage: '[layout] <filename>',
arguments: [
{name: 'layout', desc: 'Post layout. Use post, page, draft or whatever you want.'},
{name: 'filename', desc: 'Draft filename. "hello-world" for example.'}
{name: 'filename', desc: 'Draft filename relative to _drafts. The extension may be omitted when unambiguous.'}
]
}, require('./publish'));

Expand Down
103 changes: 103 additions & 0 deletions test/scripts/hexo/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,78 @@ describe('Post', () => {
await unlink(path);
});

// #1821
it('publish() - full filename in a subdirectory', async () => {
const draftDir = join(hexo.source_dir, '_drafts', 'nested-publish');
const draftPath = join(draftDir, 'Issue-1821.mkd');
const path = join(hexo.source_dir, '_posts', 'Issue-1821.md');

await writeFile(draftPath, [
'---',
'title: Issue 1821',
'---',
'content'
].join('\n'));

const result = await post.publish({
slug: 'nested-publish/Issue-1821.mkd'
});

result.path.should.eql(path);
(await exists(draftPath)).should.be.false;

await unlink(path);
await rmdir(draftDir);
});

it('publish() - does not match a filename prefix', async () => {
const draftPath = join(hexo.source_dir, '_drafts', 'Issue-1821-prefix.md');
const otherDraftPath = join(hexo.source_dir, '_drafts', 'Issue-1821-prefix-extra.md');
const path = join(hexo.source_dir, '_posts', 'Issue-1821-prefix.md');
const content = '---\ntitle: Issue 1821\n---\n';

await Promise.all([
writeFile(draftPath, content),
writeFile(otherDraftPath, content)
]);

await post.publish({ slug: 'Issue-1821-prefix' });

(await exists(draftPath)).should.be.false;
(await exists(otherDraftPath)).should.be.true;

await Promise.all([
unlink(otherDraftPath),
unlink(path)
]);
});

it('publish() - rejects an ambiguous filename without an extension', async () => {
const markdownPath = join(hexo.source_dir, '_drafts', 'Issue-1821-ambiguous.md');
const mkdPath = join(hexo.source_dir, '_drafts', 'Issue-1821-ambiguous.mkd');
const content = '---\ntitle: Issue 1821\n---\n';
let error: Error | undefined;

await Promise.all([
writeFile(markdownPath, content),
writeFile(mkdPath, content)
]);

try {
await post.publish({ slug: 'Issue-1821-ambiguous' });
} catch (err) {
error = err as Error;
}

should.exist(error);
error!.message.should.eql('Draft "Issue-1821-ambiguous" is ambiguous. Please specify the full filename: Issue-1821-ambiguous.md, Issue-1821-ambiguous.mkd.');

await Promise.all([
unlink(markdownPath),
unlink(mkdPath)
]);
});

it('publish() - layout', async () => {
const path = join(hexo.source_dir, '_posts', 'Hello-World.md');
const date = moment(now);
Expand Down Expand Up @@ -586,6 +658,37 @@ describe('Post', () => {
await rmdir(newAssetDir);
});

// #5476
it('publish() - filename with spaces and asset folder', async () => {
const draftPath = join(hexo.source_dir, '_drafts', 'space file test.md');
const assetDir = join(hexo.source_dir, '_drafts', 'space file test');
const path = join(hexo.source_dir, '_posts', 'space-file-test.md');
const newAssetDir = join(hexo.source_dir, '_posts', 'space-file-test');
hexo.config.post_asset_folder = true;

await Promise.all([
writeFile(draftPath, [
'---',
'title: Space File Test',
'---',
'content'
].join('\n')),
writeFile(join(assetDir, 'a.txt'), 'a')
]);

const result = await post.publish({
slug: 'space file test'
});

result.path.should.eql(path);
(await exists(draftPath)).should.be.false;
(await exists(assetDir)).should.be.false;
(await listDir(newAssetDir)).should.eql(['a.txt']);

await unlink(path);
await rmdir(newAssetDir);
});

// #1100
it('publish() - non-string title', async () => {
const path = join(hexo.source_dir, '_posts', '12345.md');
Expand Down
Loading