Skip to content

Commit 77df32d

Browse files
committed
fix: improve slug generation
1 parent 3c114d1 commit 77df32d

File tree

4 files changed

+49
-16
lines changed

4 files changed

+49
-16
lines changed

src/core/render/compiler/heading.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import {
66
import { slugify } from '../slugify.js';
77

88
export const headingCompiler = ({ renderer, router, compiler }) =>
9-
(renderer.heading = function ({ tokens, depth }) {
10-
const text = this.parser.parseInline(tokens);
11-
let { str, config } = getAndRemoveConfig(text);
9+
(renderer.heading = function ({ tokens, depth, text }) {
10+
const parsedText = this.parser.parseInline(tokens);
11+
let { str, config } = getAndRemoveConfig(parsedText);
1212
const nextToc = { depth, title: str };
1313

1414
const { content, ignoreAllSubs, ignoreSubHeading } =
@@ -18,7 +18,7 @@ export const headingCompiler = ({ renderer, router, compiler }) =>
1818
nextToc.title = removeAtag(str);
1919
nextToc.ignoreAllSubs = ignoreAllSubs;
2020
nextToc.ignoreSubHeading = ignoreSubHeading;
21-
const slug = slugify(config.id || str);
21+
const slug = slugify(config.id || text);
2222
const url = router.toURL(router.getCurrentPath(), { id: slug });
2323
nextToc.slug = url;
2424
compiler.toc.push(nextToc);

src/core/render/slugify.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ export function slugify(str) {
1212

1313
let slug = str
1414
.trim()
15+
.normalize('NFKD')
16+
.replace(/[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu, '')
1517
.replace(/[A-Z]+/g, lower)
1618
.replace(/<[^>]+>/g, '')
1719
.replace(re, '')
1820
.replace(/\s/g, '-')
19-
.replace(/-+/g, '-')
2021
.replace(/^(\d)/, '_$1');
2122
let count = cache[slug];
2223

src/plugins/search/search.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
getAndRemoveConfig,
33
getAndRemoveDocsifyIgnoreConfig,
4+
removeAtag,
45
} from '../../core/render/utils.js';
56
import { markdownToTxt } from './markdown-to-txt.js';
67
import Dexie from 'dexie';
@@ -110,16 +111,11 @@ export function genIndex(path, content = '', router, depth, indexKey) {
110111
if (token.type === 'heading' && token.depth <= depth) {
111112
const { str, config } = getAndRemoveConfig(token.text);
112113

113-
const text = getAndRemoveDocsifyIgnoreConfig(token.text).content;
114-
115-
if (config.id) {
116-
slug = router.toURL(path, { id: slugify(config.id) });
117-
} else {
118-
slug = router.toURL(path, { id: slugify(escapeHtml(text)) });
119-
}
114+
slug = router.toURL(path, { id: slugify(config.id || token.text) });
120115

121116
if (str) {
122117
title = getAndRemoveDocsifyIgnoreConfig(str).content;
118+
title = removeAtag(title.trim());
123119
}
124120

125121
index[slug] = {

test/unit/render-util.test.js

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,13 +168,49 @@ describe('core/render/tpl', () => {
168168

169169
describe('core/render/slugify', () => {
170170
test('slugify()', () => {
171-
const result = slugify(
171+
const htmlStrippedSlug = slugify(
172172
'Bla bla bla <svg aria-label="broken" class="broken" viewPort="0 0 1 1"><circle cx="0.5" cy="0.5"/></svg>',
173173
);
174-
const result2 = slugify(
174+
expect(htmlStrippedSlug).toBe('bla-bla-bla-');
175+
176+
const nestedHtmlStrippedSlug = slugify(
175177
'Another <span style="font-size: 1.2em" class="foo bar baz">broken <span class="aaa">example</span></span>',
176178
);
177-
expect(result).toBe('bla-bla-bla-');
178-
expect(result2).toBe('another-broken-example');
179+
expect(nestedHtmlStrippedSlug).toBe('another-broken-example');
180+
181+
const emojiRemovedSlug = slugify('emoji test ⚠️🔥✅');
182+
expect(emojiRemovedSlug).toBe('emoji-test-️');
183+
184+
const multiSpaceSlug = slugify('Title with multiple spaces');
185+
expect(multiSpaceSlug).toBe('title----with---multiple-spaces');
186+
187+
const numberLeadingSlug = slugify('123abc');
188+
expect(numberLeadingSlug).toBe('_123abc');
189+
190+
const firstDuplicate = slugify('duplicate');
191+
expect(firstDuplicate).toBe('duplicate');
192+
193+
const secondDuplicate = slugify('duplicate');
194+
expect(secondDuplicate).toBe('duplicate-1');
195+
196+
const thirdDuplicate = slugify('duplicate');
197+
expect(thirdDuplicate).toBe('duplicate-2');
198+
199+
const mixedCaseSlug = slugify('This Is Mixed CASE');
200+
expect(mixedCaseSlug).toBe('this-is-mixed-case');
201+
202+
const chinesePreservedSlug = slugify('你好 world');
203+
expect(chinesePreservedSlug).toBe('你好-world');
204+
205+
const specialCharSlug = slugify('C++ vs. Java & Python!');
206+
expect(specialCharSlug).toBe('c-vs-java--python');
207+
208+
const docsifyIgnoreSlug = slugify(
209+
'Ignore Heading <!-- {docsify-ignore} -->',
210+
);
211+
expect(docsifyIgnoreSlug).toBe('ignore-heading-');
212+
213+
const quoteCleanedSlug = slugify('"The content"');
214+
expect(quoteCleanedSlug).toBe('the-content');
179215
});
180216
});

0 commit comments

Comments
 (0)