From e25d0805505db2f1116e99d38a488d5cb39ed426 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Tue, 18 Mar 2025 09:31:13 +0530 Subject: [PATCH] fix: normalize url fragments in internal links to correctly resolve to anchors (#4628) closes #4605 - Normalizations aren't applied to raw html inside markdown or vue code. - It is assumed `slugify(slugify(something)) === slugify(something)` --- src/node/markdown/markdown.ts | 8 ++++++-- src/node/markdown/plugins/link.ts | 22 +++++++++++++++------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/node/markdown/markdown.ts b/src/node/markdown/markdown.ts index fd6bf492..bb56e0cf 100644 --- a/src/node/markdown/markdown.ts +++ b/src/node/markdown/markdown.ts @@ -13,7 +13,7 @@ import { import { sfcPlugin, type SfcPluginOptions } from '@mdit-vue/plugin-sfc' import { titlePlugin } from '@mdit-vue/plugin-title' import { tocPlugin, type TocPluginOptions } from '@mdit-vue/plugin-toc' -import { slugify } from '@mdit-vue/shared' +import { slugify as defaultSlugify } from '@mdit-vue/shared' import type { LanguageInput, ShikiTransformer, @@ -232,6 +232,8 @@ export async function createMarkdownRenderer( await options.preConfig(md) } + const slugify = options.anchor?.slugify ?? defaultSlugify + // custom plugins md.use(componentPlugin, { ...options.component }) .use(highlightLinePlugin) @@ -242,7 +244,8 @@ export async function createMarkdownRenderer( .use( linkPlugin, { target: '_blank', rel: 'noreferrer', ...options.externalLinks }, - base + base, + slugify ) .use(lineNumberPlugin, options.lineNumbers) @@ -317,6 +320,7 @@ export async function createMarkdownRenderer( } as SfcPluginOptions) .use(titlePlugin) .use(tocPlugin, { + slugify, ...options.toc } as TocPluginOptions) diff --git a/src/node/markdown/plugins/link.ts b/src/node/markdown/plugins/link.ts index 3e48843c..2fe2e1ac 100644 --- a/src/node/markdown/plugins/link.ts +++ b/src/node/markdown/plugins/link.ts @@ -16,7 +16,8 @@ const indexRE = /(^|.*\/)index.md(#?.*)$/i export const linkPlugin = ( md: MarkdownItAsync, externalAttrs: Record, - base: string + base: string, + slugify: (str: string) => string ) => { md.renderer.rules.link_open = ( tokens, @@ -27,9 +28,12 @@ export const linkPlugin = ( ) => { const token = tokens[idx] const hrefIndex = token.attrIndex('href') - const targetIndex = token.attrIndex('target') - const downloadIndex = token.attrIndex('download') - if (hrefIndex >= 0 && targetIndex < 0 && downloadIndex < 0) { + if ( + hrefIndex >= 0 && + token.attrIndex('target') < 0 && + token.attrIndex('download') < 0 && + token.attrGet('class') !== 'header-anchor' // header anchors are already normalized + ) { const hrefAttr = token.attrs![hrefIndex] const url = hrefAttr[1] if (isExternal(url)) { @@ -54,7 +58,7 @@ export const linkPlugin = ( ) { normalizeHref(hrefAttr, env) } else if (url.startsWith('#')) { - hrefAttr[1] = decodeURI(hrefAttr[1]) + hrefAttr[1] = decodeURI(normalizeHash(hrefAttr[1])) } // append base to internal (non-relative) urls @@ -72,7 +76,7 @@ export const linkPlugin = ( const indexMatch = url.match(indexRE) if (indexMatch) { const [, path, hash] = indexMatch - url = path + hash + url = path + normalizeHash(hash) } else { let cleanUrl = url.replace(/[?#].*$/, '') // transform foo.md -> foo[.html] @@ -88,7 +92,7 @@ export const linkPlugin = ( cleanUrl += '.html' } const parsed = new URL(url, 'http://a.com') - url = cleanUrl + parsed.search + parsed.hash + url = cleanUrl + parsed.search + normalizeHash(parsed.hash) } // ensure leading . for relative paths @@ -103,6 +107,10 @@ export const linkPlugin = ( hrefAttr[1] = decodeURI(url) } + function normalizeHash(str: string) { + return str ? encodeURI('#' + slugify(decodeURI(str).slice(1))) : '' + } + function pushLink(link: string, env: MarkdownEnv) { const links = env.links || (env.links = []) links.push(link)