Skip to content

Commit

Permalink
Merge 6ecb166 into d837efc
Browse files Browse the repository at this point in the history
  • Loading branch information
Mister-Hope committed Apr 12, 2024
2 parents d837efc + 6ecb166 commit b352c65
Show file tree
Hide file tree
Showing 16 changed files with 588 additions and 134 deletions.
1 change: 1 addition & 0 deletions e2e/cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export default defineConfig({
},
})
},
experimentalRunAllSpecs: true,
},
env: {
E2E_BASE: process.env.E2E_BASE ?? '/',
Expand Down
60 changes: 60 additions & 0 deletions e2e/docs/components/auto-link.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# AutoLink

<div id="route-link">
<AutoLink v-for="item in routeLinksConfig" :config="item" />
</div>

<div id="anchor-link">
<AutoLink v-for="item in anchorLinksConfig" :config="item" />
</div>

<div id="aria-label">
<AutoLink :config="{ text: 'text', link: '/', ariaLabel: 'label' }" />
</div>

<script setup lang="ts">
import { AutoLink } from 'vuepress/client'

const routeLinks = [
'/',
'/README.md',
'/index.html',
'/non-existent',
'/non-existent.md',
'/non-existent.html',
'/routes/non-ascii-paths/中文目录名/中文文件名',
'/routes/non-ascii-paths/中文目录名/中文文件名.md',
'/routes/non-ascii-paths/中文目录名/中文文件名.html',
'/README.md#hash',
'/README.md?query',
'/README.md?query#hash',
'/#hash',
'/?query',
'/?query#hash',
'#hash',
'?query',
'?query#hash',
'route-link',
'route-link.md',
'route-link.html',
'not-existent',
'not-existent.md',
'not-existent.html',
'../',
'../README.md',
'../404.md',
'../404.html',
]

const routeLinksConfig = routeLinks.map((link) => ({ link, text: 'text' }))

const anchorLinks = [
'//example.com',
'http://example.com',
'https://example.com',
'mailto:example@example.com',
'tel:+1234567890',
]

const anchorLinksConfig = anchorLinks.map((link) => ({ link, text: 'text' }))
</script>
27 changes: 27 additions & 0 deletions e2e/docs/components/route-link.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,37 @@

- <RouteLink to="/README.md" active="">text</RouteLink>
- <RouteLink to="/README.md" active>text</RouteLink>
- <RouteLink to="/" active="">text</RouteLink>
- <RouteLink to="/" active>text</RouteLink>
- <RouteLink to="/README.md" :active="false">text</RouteLink>
- <RouteLink to="/README.md">text</RouteLink>
- <RouteLink to="/" :active="false">text</RouteLink>
- <RouteLink to="/">text</RouteLink>

### Class

- <RouteLink to="/README.md" class="custom-class">text</RouteLink>
- <RouteLink to="/README.md" active class="custom-class">text</RouteLink>
- <RouteLink to="/" class="custom-class">text</RouteLink>
- <RouteLink to="/" active class="custom-class">text</RouteLink>

### Attrs

- <RouteLink to="/README.md" title="Title">text</RouteLink>
- <RouteLink to="/README.md" target="_blank">text</RouteLink>
- <RouteLink to="/README.md" rel="noopener">text</RouteLink>
- <RouteLink to="/README.md" aria-label="test">text</RouteLink>
- <RouteLink to="/" title="Title">text</RouteLink>
- <RouteLink to="/" target="_blank">text</RouteLink>
- <RouteLink to="/" rel="noopener">text</RouteLink>
- <RouteLink to="/" aria-label="test">text</RouteLink>

### Slots

- <RouteLink to="/README.md"><span>text</span></RouteLink>
- <RouteLink to="/README.md"><span>text</span><span>text</span></RouteLink>
- <RouteLink to="/"><span>text</span></RouteLink>
- <RouteLink to="/"><span>text</span><span>text</span></RouteLink>

### Hash and query

Expand All @@ -56,9 +68,24 @@
- <RouteLink to="/README.md?query=1#hash">text</RouteLink>
- <RouteLink to="/README.md?query=1&query=2#hash">text</RouteLink>
- <RouteLink to="/README.md#hash?query=1&query=2">text</RouteLink>
- <RouteLink to="/#hash">text</RouteLink>
- <RouteLink to="/?query">text</RouteLink>
- <RouteLink to="/?query#hash">text</RouteLink>
- <RouteLink to="/?query=1#hash">text</RouteLink>
- <RouteLink to="/?query=1&query=2#hash">text</RouteLink>
- <RouteLink to="/#hash?query=1&query=2">text</RouteLink>
- <RouteLink to="#hash">text</RouteLink>
- <RouteLink to="?query">text</RouteLink>
- <RouteLink to="?query#hash">text</RouteLink>
- <RouteLink to="?query=1#hash">text</RouteLink>
- <RouteLink to="?query=1&query=2#hash">text</RouteLink>
- <RouteLink to="#hash?query=1&query=2">text</RouteLink>

### Relative

- <RouteLink to="../README.md">text</RouteLink>
- <RouteLink to="../404.md">text</RouteLink>
- <RouteLink to="not-exist.md">text</RouteLink>
- <RouteLink to="../">text</RouteLink>
- <RouteLink to="../404.html">text</RouteLink>
- <RouteLink to="not-exist.html">text</RouteLink>
19 changes: 19 additions & 0 deletions e2e/tests/components/auto-link.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
it('AutoLink', () => {
cy.visit('/components/auto-link.html')

cy.get(`.e2e-theme-content #route-link a`).each((el) => {
cy.wrap(el)
.should('have.attr', 'class')
.and('match', /route-link/)
})

cy.get(`.e2e-theme-content #anchor-link a`).each((el) => {
cy.wrap(el)
.should('have.attr', 'class')
.and('match', /anchor-link/)
})

cy.get(`.e2e-theme-content #aria-label a`).each((el) => {
cy.wrap(el).should('have.attr', 'aria-label').and('eq', 'label')
})
})
26 changes: 22 additions & 4 deletions e2e/tests/components/route-link.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ it('RouteLink', () => {

cy.get(`.e2e-theme-content #active + ul > li`).each((el, index) => {
cy.wrap(el).within(() => {
if (index < 2) {
if (index < 4) {
cy.get('a').should('have.attr', 'class', 'route-link route-link-active')
} else {
cy.get('a').should('have.attr', 'class', 'route-link')
Expand All @@ -60,7 +60,7 @@ it('RouteLink', () => {
]
cy.get(`.e2e-theme-content #class + ul > li`).each((el, index) => {
cy.wrap(el).within(() => {
cy.get('a').should('have.attr', 'class', classResults[index])
cy.get('a').should('have.attr', 'class', classResults[index % 2])
cy.get('a').should('have.text', 'text')
})
})
Expand All @@ -70,22 +70,28 @@ it('RouteLink', () => {

cy.get(`.e2e-theme-content #attrs + ul > li`).each((el, index) => {
cy.wrap(el).within(() => {
cy.get('a').should('have.attr', attrName[index], attrValue[index])
cy.get('a').should('have.attr', attrName[index % 4], attrValue[index % 4])
})
})

cy.get(`.e2e-theme-content #slots + ul > li`).each((el, index) => {
cy.wrap(el).within(() => {
cy.get('a')
.children()
.should('have.lengthOf', index + 1)
.should('have.lengthOf', (index % 2) + 1)
.each((el) => {
cy.wrap(el).contains('span', 'text')
})
})
})

const HASH_AND_QUERY_RESULTS = [
`${E2E_BASE}#hash`,
`${E2E_BASE}?query`,
`${E2E_BASE}?query#hash`,
`${E2E_BASE}?query=1#hash`,
`${E2E_BASE}?query=1&query=2#hash`,
`${E2E_BASE}#hash?query=1&query=2`,
`${E2E_BASE}#hash`,
`${E2E_BASE}?query`,
`${E2E_BASE}?query#hash`,
Expand All @@ -105,4 +111,16 @@ it('RouteLink', () => {
cy.get('a').should('have.attr', 'href', HASH_AND_QUERY_RESULTS[index])
})
})

const RELATIVE_RESULTS = [
E2E_BASE,
`${E2E_BASE}404.html`,
`${E2E_BASE}components/not-exist.html`,
]

cy.get(`.e2e-theme-content #relative + ul > li`).each((el, index) => {
cy.wrap(el).within(() => {
cy.get('a').should('have.attr', 'href', RELATIVE_RESULTS[index % 3])
})
})
})
170 changes: 170 additions & 0 deletions packages/client/src/components/AutoLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { isLinkWithProtocol } from '@vuepress/shared'
import type { PropType, SlotsType, VNode } from 'vue'
import { computed, defineComponent, h, toRef } from 'vue'
import { useRoute } from 'vue-router'
import { useSiteData } from '../composables/index.js'
import { RouteLink } from './RouteLink.js'

export interface AutoLinkProps {
/**
* Text of item
*
* 项目文字
*/
text: string

/**
* Aria label of item
*
* 项目无障碍标签
*/
ariaLabel?: string

/**
* Link of item
*
* 当前页面链接
*/
link: string

/**
* Rel of `<a>` tag
*
* `<a>` 标签的 `rel` 属性
*/
rel?: string

/**
* Target of `<a>` tag
*
* `<a>` 标签的 `target` 属性
*/
target?: string

/**
* Regexp mode to be active
*
* 匹配激活的正则表达式
*/
activeMatch?: string
}

export const AutoLink = defineComponent({
name: 'AutoLink',

props: {
/**
* @description Autolink config
*/
config: {
type: Object as PropType<AutoLinkProps>,
required: true,
},

/**
* @description Whether it's active only when exact match
*/
exact: Boolean,
},

slots: Object as SlotsType<{
default?: () => VNode[] | VNode
before?: () => VNode[] | VNode | null
after?: () => VNode[] | VNode | null
}>,

setup(props, { slots }) {
const route = useRoute()
const siteData = useSiteData()

const config = toRef(props, 'config')

// If the link has non-http protocol
const withProtocol = computed(() => isLinkWithProtocol(config.value.link))

// Resolve the `target` attr
const linkTarget = computed(
() => config.value.target || (withProtocol.value ? '_blank' : undefined),
)

// If the `target` attr is "_blank"
const isBlankTarget = computed(() => linkTarget.value === '_blank')

// Whether the link is internal
const isInternal = computed(
() => !withProtocol.value && !isBlankTarget.value,
)

// Resolve the `rel` attr
const linkRel = computed(
() =>
config.value.rel ||
(isBlankTarget.value ? 'noopener noreferrer' : null),
)

// Resolve the `aria-label` attr
const linkAriaLabel = computed(
() => config.value.ariaLabel || config.value.text,
)

// Should be active when current route is a subpath of this link
const shouldBeActiveInSubpath = computed(() => {
// Should not be active in `exact` mode
if (props.exact) return false

const localeKeys = Object.keys(siteData.value.locales)

return localeKeys.length
? // Check all the locales
localeKeys.every((key) => key !== config.value.link)
: // Check root
config.value.link !== '/'
})

// If this link is active
const isActive = computed(
() =>
isInternal.value &&
(config.value.activeMatch
? new RegExp(config.value.activeMatch, 'u').test(route.path)
: // If this link is active in subpath
shouldBeActiveInSubpath.value
? route.path.startsWith(config.value.link)
: route.path === config.value.link),
)

return (): VNode => {
const { text, link } = config.value
const { before, after, default: defaultSlot } = slots

const content = defaultSlot?.() || [
before ? before() : null,
text,
after?.(),
]

return isInternal.value
? h(
RouteLink,
{
'class': 'auto-link',
'to': link,
'active': isActive.value,
'aria-label': linkAriaLabel.value,
},
() => content,
)
: h(
'a',
{
'class': 'auto-link anchor-link',
'href': link,
'rel': linkRel.value,
'target': linkTarget.value,
'aria-label': linkAriaLabel.value,
},
content,
)
}
},
})
Loading

0 comments on commit b352c65

Please sign in to comment.