mirror of
https://github.com/pengzhanbo/vuepress-theme-plume.git
synced 2026-04-23 10:58:13 +08:00
feat(theme): improve markdown link behavior (#619)
This commit is contained in:
parent
953277c77d
commit
a5c874cdcf
29
plugins/plugin-md-power/__test__/linksPlugin.spec.ts
Normal file
29
plugins/plugin-md-power/__test__/linksPlugin.spec.ts
Normal file
@ -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('<a')
|
||||
expect(md.render('[link](https://github.com)')).toContain('href="https://github.com"')
|
||||
expect(md.render('[link](https://github.com)')).toContain('target="_blank"')
|
||||
expect(md.render('[link](https://github.com)')).toContain('rel="noopener noreferrer"')
|
||||
})
|
||||
|
||||
it('should work with hash link', () => {
|
||||
expect(md.render('[link](#anchor)')).toContain('<a')
|
||||
expect(md.render('[link](#anchor)')).toContain('href="#anchor"')
|
||||
expect(md.render('[link](#anchor)')).not.toContain('target="_blank"')
|
||||
expect(md.render('[link](#anchor)')).not.toContain('rel="noopener noreferrer"')
|
||||
})
|
||||
|
||||
it('should work with internal link', () => {
|
||||
expect(md.render('[link](/path)')).toContain('<VPLink')
|
||||
expect(md.render('[link](/path)')).toContain('href="/path"')
|
||||
expect(md.render('[link](/path)')).toContain('</VPLink>')
|
||||
})
|
||||
})
|
||||
60
plugins/plugin-md-power/src/node/enhance/links.ts
Normal file
60
plugins/plugin-md-power/src/node/enhance/links.ts
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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)"
|
||||
|
||||
@ -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) {
|
||||
|
||||
<template>
|
||||
<Component
|
||||
:is="tag" class="vp-link no-icon" :class="{ link }"
|
||||
:href="link ? isExternal ? link : withBase(link) : undefined"
|
||||
:is="tag" class="vp-link" :class="{ link, 'no-icon': noIcon }"
|
||||
:href="link ? isExternalProtocol ? link : withBase(link) : undefined"
|
||||
:target="target ?? (isExternal ? '_blank' : undefined)"
|
||||
:rel="rel ?? (isExternal ? 'noreferrer' : undefined)"
|
||||
@click="linkTo($event)"
|
||||
@ -37,12 +37,12 @@ function linkTo(e: Event) {
|
||||
<slot>
|
||||
{{ text || href }}
|
||||
</slot>
|
||||
<span v-if="isExternal && !noIcon" class="vpi-external-link icon" />
|
||||
<span v-if="isExternal && !noIcon" class="vpi-external-link" />
|
||||
</Component>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
<style>
|
||||
.vp-link .vpi-external-link {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
margin-top: -1px;
|
||||
|
||||
@ -1,11 +1,19 @@
|
||||
import type { ComputedRef, MaybeRefOrGetter } from 'vue'
|
||||
import { isLinkExternal } from '@vuepress/helper/client'
|
||||
import { isLinkExternal, isLinkWithProtocol } from '@vuepress/helper/client'
|
||||
import { computed, toValue } from 'vue'
|
||||
import { resolveRouteFullPath, useRoute } from 'vuepress/client'
|
||||
import { useData } from './data.js'
|
||||
|
||||
interface UseLinkResult {
|
||||
/**
|
||||
* 外部链接
|
||||
*/
|
||||
isExternal: ComputedRef<boolean>
|
||||
/**
|
||||
* 外部链接协议
|
||||
* 此项不包含 target="_blank" 的情况
|
||||
*/
|
||||
isExternalProtocol: ComputedRef<boolean>
|
||||
link: ComputedRef<string | undefined>
|
||||
}
|
||||
|
||||
@ -46,5 +54,12 @@ export function useLink(
|
||||
return path
|
||||
})
|
||||
|
||||
return { isExternal, link }
|
||||
const isExternalProtocol = computed(() => {
|
||||
if (!link.value || link.value[0] === '#')
|
||||
return false
|
||||
|
||||
return isLinkWithProtocol(link.value)
|
||||
})
|
||||
|
||||
return { isExternal, isExternalProtocol, link }
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import VPLinkCard from '@theme/global/VPLinkCard.vue'
|
||||
import VPHomeBox from '@theme/Home/VPHomeBox.vue'
|
||||
import VPButton from '@theme/VPButton.vue'
|
||||
import VPIcon from '@theme/VPIcon.vue'
|
||||
import VPLink from '@theme/VPLink.vue'
|
||||
import { hasGlobalComponent } from '@vuepress/helper/client'
|
||||
import { h, resolveComponent } from 'vue'
|
||||
|
||||
@ -30,6 +31,8 @@ export function globalComponents(app: App): void {
|
||||
app.component('VPCardMasonry', VPCardMasonry)
|
||||
app.component('CardMasonry', VPCardMasonry)
|
||||
|
||||
app.component('VPLink', VPLink)
|
||||
|
||||
app.component('Icon', VPIcon)
|
||||
app.component('VPIcon', VPIcon)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user