From ca3a1e1c692c6d7013b0cf9639ded394046762c3 Mon Sep 17 00:00:00 2001 From: dimaslanjaka Date: Tue, 6 May 2025 02:27:37 +0700 Subject: [PATCH] feat(post_permalink): `:filepath` pattern support --- lib/plugins/filter/post_permalink.ts | 26 ++++- test/scripts/filters/filepath_permalink.ts | 115 +++++++++++++++++++++ 2 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 test/scripts/filters/filepath_permalink.ts diff --git a/lib/plugins/filter/post_permalink.ts b/lib/plugins/filter/post_permalink.ts index d924ecd9..09dbdb67 100644 --- a/lib/plugins/filter/post_permalink.ts +++ b/lib/plugins/filter/post_permalink.ts @@ -19,14 +19,29 @@ function postPermalinkFilter(this: Hexo, data: PostSchema): string { return __permalink; } - const hash = slug && date - ? createSha1Hash().update(slug + date.unix().toString()).digest('hex').slice(0, 12) - : null; + const hash + = slug && date + ? createSha1Hash() + .update(slug + date.unix().toString()) + .digest('hex') + .slice(0, 12) + : null; + + const relativeSourcePath = data.full_source + // Remove base directory + .replace(this.base_dir, '') + // Normalize path to handle both / and \ (cross-platform) + .replace(/\\/g, '/') + // Remove leading "source/" or "source/_posts/" if present + .replace(/^source\/(_posts\/)?/, '') + // Remove any extension + .replace(/\.[^/.]+$/, ''); + const meta = { id: id || _id, title: slug, name: typeof slug === 'string' ? basename(slug) : '', - post_title: slugize(title, {transform: 1}), + post_title: slugize(title, { transform: 1 }), year: date.format('YYYY'), month: date.format('MM'), day: date.format('DD'), @@ -37,7 +52,8 @@ function postPermalinkFilter(this: Hexo, data: PostSchema): string { i_day: date.format('D'), timestamp: date.format('X'), hash, - category: config.default_category + category: config.default_category, + filepath: relativeSourcePath }; if (!permalink || permalink.rule !== config.permalink) { diff --git a/test/scripts/filters/filepath_permalink.ts b/test/scripts/filters/filepath_permalink.ts new file mode 100644 index 00000000..ab9364b0 --- /dev/null +++ b/test/scripts/filters/filepath_permalink.ts @@ -0,0 +1,115 @@ +import { should } from 'chai'; +import Hexo from 'hexo'; +import moment from 'moment'; +import path from 'path'; +import postPermalinkFilter from '../../../lib/plugins/filter/post_permalink'; + +// Usage: `mocha test/scripts/filters/filepath_permalink.ts --require ts-node/register` +should(); // <-- This line is essential to enable `.should` assertions + +type PostPermalinkFilterParams = Parameters; +type PostPermalinkFilterReturn = ReturnType; + +describe('Hexo Filter (TypeScript)', () => { + // Initialize hexo + const hexo = new Hexo(path.join(__dirname, '../../fixtures'), { silent: true }); + // Initialize permalink parser + const postPermalink: (...args: PostPermalinkFilterParams) => PostPermalinkFilterReturn + = postPermalinkFilter.bind(hexo); + // Initialize post model + const Post = hexo.model('Post'); + + before(async () => { + // Load configurations + await hexo.init(); + }); + + it(':filepath.html', async () => { + hexo.config.permalink = ':filepath.html'; + + const posts = await Post.insert([ + { + source: 'foo.md', + slug: 'foo', + date: moment('2014-01-02') + }, + { + source: '2025/01/my-foo.md', + slug: '2025/01/my-foo', + date: moment('2014-01-04') + } + ]); + postPermalink(posts[0]).should.eql('foo.html'); + postPermalink(posts[1]).should.eql('2025/01/my-foo.html'); + }); + + it('filepath - inside folder', async () => { + hexo.config.permalink = ':year/:month/:day/:filepath/'; + + const post = await Post.insert({ + source: 'sub/2025-05-06-my-new-post.md', + slug: '2025-05-06-my-new-post', + title: 'My New Post', + date: moment('2025-05-06') + }); + postPermalink(post).should.eql('2025/05/06/sub/2025-05-06-my-new-post/'); + Post.removeById(post._id); + }); + + it('filepath - within spaces', async () => { + hexo.config.permalink = ':year/:month/:filepath.html'; + + const post = await Post.insert({ + source: 'space folder/my new post with space.md', + slug: 'space folder/my new post with space', + title: 'My New Post With Space', + date: moment('2025-10-07') + }); + postPermalink(post).should.eql('2025/10/space folder/my new post with space.html'); + Post.removeById(post._id); + }); + + it('filepath - with language', async () => { + hexo.config.permalink = 'posts/:lang/:filepath/'; + hexo.config.permalink_defaults = { lang: 'en' }; + + const posts = await Post.insert([ + { + source: 'my-new-post.md', + slug: 'my-new-post', + title: 'My New Post1' + }, + { + source: 'my-new-fr-post.md', + slug: 'my-new-fr-post', + title: 'My New Post2', + lang: 'fr' + } + ]); + postPermalink(posts[0]).should.eql('posts/en/my-new-post/'); + postPermalink(posts[1]).should.eql('posts/fr/my-new-fr-post/'); + }); + + it('permalink - should override everything', async () => { + hexo.config.permalink = ':year/:month/:day/:filepath/'; + + const posts = await Post.insert([{ + source: 'my-new-post.md', + slug: 'hexo/permalink-test-filepath', + __permalink: 'hexo/permalink-test-filepath', + title: 'Permalink Test', + date: moment('2014-01-02') + }, { + source: 'another-new-post.md', + slug: '/hexo-hexo/permalink-test-2', + __permalink: '/hexo-hexo/permalink-test-2', + title: 'Permalink Test', + date: moment('2014-01-02') + }]); + + postPermalink(posts[0]).should.eql('/hexo/permalink-test-filepath'); + postPermalink(posts[1]).should.eql('/hexo-hexo/permalink-test-2'); + + await Promise.all(posts.map(post => Post.removeById(post._id))); + }); +});