Skip to content

Commit e25d080

Browse files
authored
fix: normalize url fragments in internal links to correctly resolve to anchors (vuejs#4628)
closes vuejs#4605 - Normalizations aren't applied to raw html inside markdown or vue code. - It is assumed `slugify(slugify(something)) === slugify(something)`
1 parent e06b83e commit e25d080

File tree

2 files changed

+21
-9
lines changed

2 files changed

+21
-9
lines changed

src/node/markdown/markdown.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import { sfcPlugin, type SfcPluginOptions } from '@mdit-vue/plugin-sfc'
1414
import { titlePlugin } from '@mdit-vue/plugin-title'
1515
import { tocPlugin, type TocPluginOptions } from '@mdit-vue/plugin-toc'
16-
import { slugify } from '@mdit-vue/shared'
16+
import { slugify as defaultSlugify } from '@mdit-vue/shared'
1717
import type {
1818
LanguageInput,
1919
ShikiTransformer,
@@ -232,6 +232,8 @@ export async function createMarkdownRenderer(
232232
await options.preConfig(md)
233233
}
234234

235+
const slugify = options.anchor?.slugify ?? defaultSlugify
236+
235237
// custom plugins
236238
md.use(componentPlugin, { ...options.component })
237239
.use(highlightLinePlugin)
@@ -242,7 +244,8 @@ export async function createMarkdownRenderer(
242244
.use(
243245
linkPlugin,
244246
{ target: '_blank', rel: 'noreferrer', ...options.externalLinks },
245-
base
247+
base,
248+
slugify
246249
)
247250
.use(lineNumberPlugin, options.lineNumbers)
248251

@@ -317,6 +320,7 @@ export async function createMarkdownRenderer(
317320
} as SfcPluginOptions)
318321
.use(titlePlugin)
319322
.use(tocPlugin, {
323+
slugify,
320324
...options.toc
321325
} as TocPluginOptions)
322326

src/node/markdown/plugins/link.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ const indexRE = /(^|.*\/)index.md(#?.*)$/i
1616
export const linkPlugin = (
1717
md: MarkdownItAsync,
1818
externalAttrs: Record<string, string>,
19-
base: string
19+
base: string,
20+
slugify: (str: string) => string
2021
) => {
2122
md.renderer.rules.link_open = (
2223
tokens,
@@ -27,9 +28,12 @@ export const linkPlugin = (
2728
) => {
2829
const token = tokens[idx]
2930
const hrefIndex = token.attrIndex('href')
30-
const targetIndex = token.attrIndex('target')
31-
const downloadIndex = token.attrIndex('download')
32-
if (hrefIndex >= 0 && targetIndex < 0 && downloadIndex < 0) {
31+
if (
32+
hrefIndex >= 0 &&
33+
token.attrIndex('target') < 0 &&
34+
token.attrIndex('download') < 0 &&
35+
token.attrGet('class') !== 'header-anchor' // header anchors are already normalized
36+
) {
3337
const hrefAttr = token.attrs![hrefIndex]
3438
const url = hrefAttr[1]
3539
if (isExternal(url)) {
@@ -54,7 +58,7 @@ export const linkPlugin = (
5458
) {
5559
normalizeHref(hrefAttr, env)
5660
} else if (url.startsWith('#')) {
57-
hrefAttr[1] = decodeURI(hrefAttr[1])
61+
hrefAttr[1] = decodeURI(normalizeHash(hrefAttr[1]))
5862
}
5963

6064
// append base to internal (non-relative) urls
@@ -72,7 +76,7 @@ export const linkPlugin = (
7276
const indexMatch = url.match(indexRE)
7377
if (indexMatch) {
7478
const [, path, hash] = indexMatch
75-
url = path + hash
79+
url = path + normalizeHash(hash)
7680
} else {
7781
let cleanUrl = url.replace(/[?#].*$/, '')
7882
// transform foo.md -> foo[.html]
@@ -88,7 +92,7 @@ export const linkPlugin = (
8892
cleanUrl += '.html'
8993
}
9094
const parsed = new URL(url, 'http://a.com')
91-
url = cleanUrl + parsed.search + parsed.hash
95+
url = cleanUrl + parsed.search + normalizeHash(parsed.hash)
9296
}
9397

9498
// ensure leading . for relative paths
@@ -103,6 +107,10 @@ export const linkPlugin = (
103107
hrefAttr[1] = decodeURI(url)
104108
}
105109

110+
function normalizeHash(str: string) {
111+
return str ? encodeURI('#' + slugify(decodeURI(str).slice(1))) : ''
112+
}
113+
106114
function pushLink(link: string, env: MarkdownEnv) {
107115
const links = env.links || (env.links = [])
108116
links.push(link)

0 commit comments

Comments
 (0)