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) { -