diff --git a/plugins/plugin-md-power/__test__/linksPlugin.spec.ts b/plugins/plugin-md-power/__test__/linksPlugin.spec.ts
new file mode 100644
index 00000000..34401a4a
--- /dev/null
+++ b/plugins/plugin-md-power/__test__/linksPlugin.spec.ts
@@ -0,0 +1,29 @@
+import MarkdownIt from 'markdown-it'
+import { describe, expect, it } from 'vitest'
+import { linksPlugin } from '../src/node/enhance/links.js'
+
+describe('linksPlugin', () => {
+ const md = new MarkdownIt({
+ html: true,
+ }).use(linksPlugin)
+
+ it('should work with external link', () => {
+ expect(md.render('[link](https://github.com)')).toContain(' {
+ expect(md.render('[link](#anchor)')).toContain(' {
+ expect(md.render('[link](/path)')).toContain('')
+ })
+})
diff --git a/plugins/plugin-md-power/src/node/enhance/links.ts b/plugins/plugin-md-power/src/node/enhance/links.ts
new file mode 100644
index 00000000..176b0494
--- /dev/null
+++ b/plugins/plugin-md-power/src/node/enhance/links.ts
@@ -0,0 +1,60 @@
+import type Token from 'markdown-it/lib/token.mjs'
+import type { Markdown, MarkdownEnv } from 'vuepress/markdown'
+import { isLinkWithProtocol } from 'vuepress/shared'
+
+export function linksPlugin(md: Markdown): void {
+ // attrs that going to be added to external links
+ const externalAttrs = {
+ target: '_blank',
+ rel: 'noopener noreferrer',
+ }
+
+ let hasOpenInternalLink = false
+ const internalTag = 'VPLink'
+
+ function handleLinkOpen(tokens: Token[], idx: number) {
+ hasOpenInternalLink = false
+ const token = tokens[idx]
+ // get `href` attr index
+ const hrefIndex = token.attrIndex('href')
+
+ // if `href` attr does not exist, skip
+ /* istanbul ignore if -- @preserve */
+ if (hrefIndex < 0) {
+ return
+ }
+
+ // if `href` attr exists, `token.attrs` is not `null`
+ const hrefAttr = token.attrs![hrefIndex]
+ const hrefLink: string = hrefAttr[1]
+
+ if (isLinkWithProtocol(hrefLink)) {
+ // set `externalAttrs` to current token
+ Object.entries(externalAttrs).forEach(([key, val]) => {
+ token.attrSet(key, val)
+ })
+ return
+ }
+
+ if (hrefLink[0] === '#')
+ return
+
+ // convert starting tag of internal link
+ hasOpenInternalLink = true
+ token.tag = internalTag
+ }
+
+ md.renderer.rules.link_open = (tokens, idx, opts, env: MarkdownEnv, self) => {
+ handleLinkOpen(tokens, idx)
+ return self.renderToken(tokens, idx, opts)
+ }
+
+ md.renderer.rules.link_close = (tokens, idx, opts, _env, self) => {
+ // convert ending tag of internal link
+ if (hasOpenInternalLink) {
+ hasOpenInternalLink = false
+ tokens[idx].tag = internalTag
+ }
+ return self.renderToken(tokens, idx, opts)
+ }
+}
diff --git a/plugins/plugin-md-power/src/node/plugin.ts b/plugins/plugin-md-power/src/node/plugin.ts
index 6c4e97c3..90ab7d14 100644
--- a/plugins/plugin-md-power/src/node/plugin.ts
+++ b/plugins/plugin-md-power/src/node/plugin.ts
@@ -8,6 +8,7 @@ import { demoPlugin, demoWatcher, extendsPageWithDemo, waitDemoRender } from './
import { embedSyntaxPlugin } from './embed/index.js'
import { docsTitlePlugin } from './enhance/docsTitle.js'
import { imageSizePlugin } from './enhance/imageSize.js'
+import { linksPlugin } from './enhance/links.js'
import { iconPlugin } from './icon/index.js'
import { inlineSyntaxPlugin } from './inline/index.js'
import { prepareConfigFile } from './prepareConfigFile.js'
@@ -44,6 +45,7 @@ export function markdownPowerPlugin(
},
extendsMarkdown: async (md, app) => {
+ linksPlugin(md)
docsTitlePlugin(md)
embedSyntaxPlugin(md, options)
inlineSyntaxPlugin(md, options)
diff --git a/theme/src/client/components/VPButton.vue b/theme/src/client/components/VPButton.vue
index 58e26975..46d81804 100644
--- a/theme/src/client/components/VPButton.vue
+++ b/theme/src/client/components/VPButton.vue
@@ -26,7 +26,7 @@ const component = computed(() => {
return props.tag || props.href ? 'a' : 'button'
})
-const { link, isExternal } = useLink(toRef(props, 'href'), toRef(props, 'target'))
+const { link, isExternal, isExternalProtocol } = useLink(toRef(props, 'href'), toRef(props, 'target'))
function linkTo(e: Event) {
if (!isExternal.value && link.value?.[0] !== '#') {
@@ -42,7 +42,7 @@ function linkTo(e: Event) {
:is="component"
class="vp-button"
:class="[size, theme]"
- :href=" link ? link[0] === '#' || isExternal ? link : withBase(link) : undefined"
+ :href=" link ? link[0] === '#' || isExternalProtocol ? link : withBase(link) : undefined"
:target="target ?? (isExternal ? '_blank' : undefined)"
:rel="rel ?? (isExternal ? 'noreferrer' : undefined)"
@click="linkTo($event)"
diff --git a/theme/src/client/components/VPLink.vue b/theme/src/client/components/VPLink.vue
index 2d703119..c3616c44 100644
--- a/theme/src/client/components/VPLink.vue
+++ b/theme/src/client/components/VPLink.vue
@@ -16,7 +16,7 @@ const router = useRouter()
const tag = computed(() => props.tag ?? (props.href ? 'a' : 'span'))
-const { link, isExternal } = useLink(toRef(props, 'href'), toRef(props, 'target'))
+const { link, isExternal, isExternalProtocol } = useLink(toRef(props, 'href'), toRef(props, 'target'))
function linkTo(e: Event) {
if (!isExternal.value && link.value) {
@@ -28,8 +28,8 @@ function linkTo(e: Event) {
{{ text || href }}
-
+
-