From 1bdf1c0b2bbf990ea033be418efeda3d11ec8e63 Mon Sep 17 00:00:00 2001 From: Luffy <52o@qq52o.cn> Date: Mon, 28 Jul 2025 13:39:23 +0800 Subject: [PATCH] fix: improve slug generation --- src/core/render/compiler/heading.js | 8 +++--- src/core/render/slugify.js | 3 +- src/plugins/search/search.js | 10 ++----- test/unit/render-util.test.js | 44 ++++++++++++++++++++++++++--- 4 files changed, 49 insertions(+), 16 deletions(-) diff --git a/src/core/render/compiler/heading.js b/src/core/render/compiler/heading.js index c00a75976..9ea804a64 100644 --- a/src/core/render/compiler/heading.js +++ b/src/core/render/compiler/heading.js @@ -7,9 +7,9 @@ import { slugify } from '../slugify.js'; import { stripUrlExceptId } from '../../router/util.js'; export const headingCompiler = ({ renderer, router, compiler }) => - (renderer.heading = function ({ tokens, depth }) { - const text = this.parser.parseInline(tokens); - let { str, config } = getAndRemoveConfig(text); + (renderer.heading = function ({ tokens, depth, text }) { + const parsedText = this.parser.parseInline(tokens); + let { str, config } = getAndRemoveConfig(parsedText); const nextToc = { depth, title: str }; const { content, ignoreAllSubs, ignoreSubHeading } = @@ -19,7 +19,7 @@ export const headingCompiler = ({ renderer, router, compiler }) => nextToc.title = removeAtag(str); nextToc.ignoreAllSubs = ignoreAllSubs; nextToc.ignoreSubHeading = ignoreSubHeading; - const slug = slugify(config.id || str); + const slug = slugify(config.id || text); const url = router.toURL(router.getCurrentPath(), { id: slug }); nextToc.slug = stripUrlExceptId(url); compiler.toc.push(nextToc); diff --git a/src/core/render/slugify.js b/src/core/render/slugify.js index 7a6bf8be5..ff910d684 100644 --- a/src/core/render/slugify.js +++ b/src/core/render/slugify.js @@ -12,11 +12,12 @@ export function slugify(str) { let slug = str .trim() + .normalize('NFKD') + .replace(/[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu, '') .replace(/[A-Z]+/g, lower) .replace(/<[^>]+>/g, '') .replace(re, '') .replace(/\s/g, '-') - .replace(/-+/g, '-') .replace(/^(\d)/, '_$1'); let count = cache[slug]; diff --git a/src/plugins/search/search.js b/src/plugins/search/search.js index 404e23b4b..bb0dfb55d 100644 --- a/src/plugins/search/search.js +++ b/src/plugins/search/search.js @@ -1,6 +1,7 @@ import { getAndRemoveConfig, getAndRemoveDocsifyIgnoreConfig, + removeAtag, } from '../../core/render/utils.js'; import { markdownToTxt } from './markdown-to-txt.js'; import Dexie from 'dexie'; @@ -110,16 +111,11 @@ export function genIndex(path, content = '', router, depth, indexKey) { if (token.type === 'heading' && token.depth <= depth) { const { str, config } = getAndRemoveConfig(token.text); - const text = getAndRemoveDocsifyIgnoreConfig(token.text).content; - - if (config.id) { - slug = router.toURL(path, { id: slugify(config.id) }); - } else { - slug = router.toURL(path, { id: slugify(escapeHtml(text)) }); - } + slug = router.toURL(path, { id: slugify(config.id || token.text) }); if (str) { title = getAndRemoveDocsifyIgnoreConfig(str).content; + title = removeAtag(title.trim()); } index[slug] = { diff --git a/test/unit/render-util.test.js b/test/unit/render-util.test.js index 6c4218254..3ff0c1a70 100644 --- a/test/unit/render-util.test.js +++ b/test/unit/render-util.test.js @@ -168,13 +168,49 @@ describe('core/render/tpl', () => { describe('core/render/slugify', () => { test('slugify()', () => { - const result = slugify( + const htmlStrippedSlug = slugify( 'Bla bla bla ', ); - const result2 = slugify( + expect(htmlStrippedSlug).toBe('bla-bla-bla-'); + + const nestedHtmlStrippedSlug = slugify( 'Another broken example', ); - expect(result).toBe('bla-bla-bla-'); - expect(result2).toBe('another-broken-example'); + expect(nestedHtmlStrippedSlug).toBe('another-broken-example'); + + const emojiRemovedSlug = slugify('emoji test ⚠️🔥✅'); + expect(emojiRemovedSlug).toBe('emoji-test-️'); + + const multiSpaceSlug = slugify('Title with multiple spaces'); + expect(multiSpaceSlug).toBe('title----with---multiple-spaces'); + + const numberLeadingSlug = slugify('123abc'); + expect(numberLeadingSlug).toBe('_123abc'); + + const firstDuplicate = slugify('duplicate'); + expect(firstDuplicate).toBe('duplicate'); + + const secondDuplicate = slugify('duplicate'); + expect(secondDuplicate).toBe('duplicate-1'); + + const thirdDuplicate = slugify('duplicate'); + expect(thirdDuplicate).toBe('duplicate-2'); + + const mixedCaseSlug = slugify('This Is Mixed CASE'); + expect(mixedCaseSlug).toBe('this-is-mixed-case'); + + const chinesePreservedSlug = slugify('你好 world'); + expect(chinesePreservedSlug).toBe('你好-world'); + + const specialCharSlug = slugify('C++ vs. Java & Python!'); + expect(specialCharSlug).toBe('c-vs-java--python'); + + const docsifyIgnoreSlug = slugify( + 'Ignore Heading ', + ); + expect(docsifyIgnoreSlug).toBe('ignore-heading-'); + + const quoteCleanedSlug = slugify('"The content"'); + expect(quoteCleanedSlug).toBe('the-content'); }); });