Skip to content

Commit

Permalink
fix: handle dynamic to property for links
Browse files Browse the repository at this point in the history
  • Loading branch information
egoist committed Sep 29, 2019
1 parent 1fa4873 commit 21ac524
Show file tree
Hide file tree
Showing 14 changed files with 163 additions and 58 deletions.
50 changes: 50 additions & 0 deletions packages/saber/vue-renderer/app/components/SaberLink.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import isAbsoluteUrl from '../utils/is-absolute-url'

const setAttribute = (attrs, name, value) => {
if (attrs[name] === undefined) {
attrs[name] = value
}
}

const HTTP_RE = /^https?:\/\//

export default {
name: 'SaberLink',

functional: true,

render(h, { data, children, parent }) {
const attrs = { ...data.attrs }
const isExternal = typeof attrs.to === 'string' && isAbsoluteUrl(attrs.to)

if (isExternal) {
if (HTTP_RE.test(attrs.to)) {
setAttribute(attrs, 'rel', 'noopener noreferrer')

if (attrs.openLinkInNewTab !== false) {
setAttribute(attrs, 'target', '_blank')
}
}
attrs.href = attrs.to
delete attrs.to
} else {
if (typeof attrs.to === 'string') {
attrs.to = parent.$saber.getPageLink(attrs.to)
} else {
const { route } = parent.$router.resolve(attrs.to)
attrs.to = parent.$saber.getPageLink(route.fullPath)
}
}

delete attrs.openLinkInNewTab

return h(
isExternal ? 'a' : 'router-link',
{
...data,
attrs
},
children
)
}
}
15 changes: 12 additions & 3 deletions packages/saber/vue-renderer/app/create-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import layouts from '#cache/layouts'
import createRouter from './router'
import Layout from './components/LayoutManager.vue'
import ClientOnly from './components/ClientOnly'
import SaberLink from './components/SaberLink'
import extendBrowserApi from '#cache/extend-browser-api'
import { join, dirname } from './helpers/path'
import injectConfig from './helpers/inject-config'
Expand All @@ -16,6 +17,7 @@ Vue.config.productionTip = false

Vue.component(ClientOnly.name, ClientOnly)
Vue.component(Layout.name, Layout)
Vue.component(SaberLink.name, SaberLink)

Vue.use(Meta, {
keyName: 'head',
Expand Down Expand Up @@ -122,9 +124,16 @@ export default context => {
},

getPageLink(link) {
const matched = Array.isArray(link)
? link // The link is already parsed
: /^([^#?]+)([#?].*)?$/.exec(link)
if (typeof link !== 'string' && process.env.NODE_ENV !== 'production') {
throw new TypeError(`Expect link to be a string`)
}

// Already a route path, directly return
if (/^\//.test(link)) {
return link
}

const matched = /^([^#?]+)([#?].*)?$/.exec(link)

if (!matched) {
return link
Expand Down
1 change: 0 additions & 1 deletion packages/saber/vue-renderer/app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import routes from '#cache/routes'
Vue.use(Router)
// Make `<RouterLink>` prefetch-able
Vue.use(RoutePrefetch, {
componentName: 'SaberLink',
// Only enable prefetching in production mode
prefetch: process.env.NODE_ENV === 'production'
})
Expand Down
14 changes: 14 additions & 0 deletions packages/saber/vue-renderer/app/utils/is-absolute-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default url => {
if (typeof url !== 'string') {
throw new TypeError(`Expected a \`string\`, got \`${typeof url}\``)
}

// Don't match Windows paths `c:\`
if (/^[a-zA-Z]:\\/.test(url)) {
return false
}

// Scheme: https://tools.ietf.org/html/rfc3986#section-3.1
// Absolute URL: https://tools.ietf.org/html/rfc3986#section-4.3
return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url)
}
2 changes: 2 additions & 0 deletions packages/saber/vue-renderer/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ class VueRenderer {
prettify: false
},
api.webpackUtils.getCacheOptions('vue-loader', {
// Increse `key` to invalid cache
key: 0,
type,
'vue-loader': require('vue-loader/package.json').version,
'vue-template-compiler': require('vue-template-compiler/package.json')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ test('basic', async () => {
<saber-link :to="foo">foo</saber-link>
`)
expect(html).toBe(`
<saber-link :to="$saber.getPageLink('foo')">foo</saber-link>
<a target="_blank" rel="noopener noreferrer" href="https://example.com">foo</a>
<a href="mailto:[email protected]">foo</a>
<saber-link :to="$saber.getPageLink('/foo')">foo</saber-link>
<saber-link to="foo">foo</saber-link>
<saber-link to="https://example.com">foo</saber-link>
<saber-link to="mailto:[email protected]">foo</saber-link>
<saber-link to="/foo">foo</saber-link>
<saber-link :to="foo">foo</saber-link>
`)
})
Expand Down
54 changes: 31 additions & 23 deletions packages/saber/vue-renderer/lib/template-plugins/link.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
const { isAbsoluteUrl } = require('saber-utils')
const getAttribute = (node, name) => {
if (node.attrs[name] !== undefined) {
return { value: node.attrs[name], isStatic: true }
}

return {
value: node.attrs[`:${name}`] || node.attrs[`v-bind:${name}`],
isStatic: false
}
}

const removeAttribute = (node, name) => {
delete node.attrs[name]
delete node.attrs[`:${name}`]
delete node.attrs[`v-bind:${name}`]
}

module.exports = ({ openLinkInNewTab = true } = {}) => tree => {
tree.walk(node => {
Expand All @@ -9,31 +24,24 @@ module.exports = ({ openLinkInNewTab = true } = {}) => tree => {
return node
}

if (node.tag === 'a' && node.attrs.href) {
if (isAbsoluteUrl(node.attrs.href)) {
// Add attributes for external link
if (/^https?:\/\//.test(node.attrs.href)) {
node.attrs = Object.assign(
{
target: openLinkInNewTab ? '_blank' : undefined,
rel: 'noopener noreferrer'
},
node.attrs
)
}
const href = getAttribute(node, 'href')

if (node.tag === 'a' && href.value) {
node.tag = 'saber-link'
if (href.isStatic) {
node.attrs.to = href.value
} else {
// Convert internal `<a>` to `<saber-link>`
node.tag = 'saber-link'
// Resolve link using `getPageLink`
node.attrs[':to'] = `$saber.getPageLink('${node.attrs.href}')`
delete node.attrs.href
node.attrs[':to'] = href.value
}
}

// Resolve link using `getPageLink`
if (node.tag === 'saber-link' && node.attrs.to) {
node.attrs[':to'] = `$saber.getPageLink('${node.attrs.to}')`
delete node.attrs.to
removeAttribute(node, 'href')

if (
openLinkInNewTab === false &&
getAttribute(node, 'openLinkInNewTab').value === undefined
) {
node.attrs[':openLinkInNewTab'] = JSON.stringify(openLinkInNewTab)
}
}

return node
Expand Down
43 changes: 33 additions & 10 deletions website/pages/docs/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,54 @@ title: Routing
layout: docs
---

## Client-side transitions with `<saber-link>`
## Client-side transitions with `<a>` element

In Saber, client-side transitions between routes can be enabled via a `<saber-link>` component. It's quite similar to Vue Router's `<router-link>` component but with more features like page prefetching.
In Saber, client-side transitions between routes can be enabled via `<a>` elements:

Basic example, `./pages/index.vue`:

```vue
<template>
<div>
<h1>Welcome to Saber!</h1>
<saber-link to="/about.html">About</saber-link>
<a href="/about.html">About</a>
</div>
</template>
```

Note that when you are using Markdown pages, internal links will be converted to `<saber-link>` automatically, and you can also reference pages using relative path, for example, `./pages/about.md`:
It also works in Markdown since links are transformed to `<a>` elements as well.

```markdown
[Contact us](./contact.md)
Internally, `<a>` elements are converted to a built-in component [`<saber-link>`](components.md#saberlink), so these are equivalent:

```vue
<a href="/about.html">About</a>
<saber-link to="/about.html">About</saber-link>
```

`<saber-link>` will be rendered as `<a target="_blank" rel="noopener noreferrer">` element if the link is an absolute URL (like `https://github.com`), otherwise it's rendered as Vue Router's `<router-link>` component.

## Reference local pages

You can use `<a>` element to reference local pages by filename:

```vue
<a href="./about.md">About</a>
```

..is converted to:

```html
<saber-link to="/about.html">About</saber-link>
```

This will be converted to:
This is useful if you're not sure what the permalink is or you might change the permalink format in the future.

## Disable this with `saber-ignore`

If you dont' want to use `<a>` for client-side transitions, you can use the `saber-ignore` attribute:

```vue
<saber-link :to="$saber.getPageLink('./contact.md')">
Contact us
</saber-link>
<a saber-ignore href="/">Home</a>
```

Then this will be rendered as `<a>` instead of `<router-link>`, it will make the browser fully reload the page.
4 changes: 2 additions & 2 deletions website/src/components/Header.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
</svg>
</div>
<h1 class="logo">
<saber-link to="/">
<a href="/">
<Logo />
<span>Saber</span>
</saber-link>
</a>
</h1>
</div>
<div class="header-right">
Expand Down
2 changes: 1 addition & 1 deletion website/src/components/PostList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div class="posts">
<div class="post" v-for="post in posts" :key="post.permalink">
<h2 class="post-title">
<saber-link :to="post.permalink">{{ post.title }}</saber-link>
<a :href="post.permalink">{{ post.title }}</a>
</h2>
<PostMeta :page="post" />
<div class="post-excerpt" v-html="post.excerpt"></div>
Expand Down
6 changes: 3 additions & 3 deletions website/src/components/Sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@
<transition name="fade">
<div class="item-children" v-if="isExpanded(item.children)">
<div class="item-child" v-for="(childItem, i) in item.children" :key="i">
<saber-link
:to="childItem.link"
<a
:href="childItem.link"
:class="{active: isActive(childItem.link)}"
>{{ childItem.title }}</saber-link>
>{{ childItem.title }}</a>
</div>
</div>
</transition>
Expand Down
8 changes: 4 additions & 4 deletions website/src/components/SiteNav.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
<template>
<ul class="nav">
<li class="nav-item">
<saber-link to="/docs">Guide</saber-link>
<a href="/docs">Guide</a>
</li>
<li class="nav-item">
<saber-link to="/tutorial/tutorial.html">Tutorial</saber-link>
<a href="/tutorial/tutorial.html">Tutorial</a>
</li>
<li class="nav-item">
<saber-link to="/themes">Themes</saber-link>
<a href="/themes">Themes</a>
</li>
<li class="nav-item">
<saber-link to="/blog/">Blog</saber-link>
<a href="/blog/">Blog</a>
</li>
<li class="nav-item">
<a href="https://chat.saber.land" target="_blank" class="discord-link">
Expand Down
6 changes: 3 additions & 3 deletions website/src/components/Toc.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
</div>
<div class="toc-title">Contents</div>
<div class="toc-headings">
<saber-link
<a
:data-level="heading.level"
:class="{'toc-heading': true, 'active-hash': `#${heading.slug}` === currentHash}"
v-for="heading in filteredHeadings"
:key="heading.slug"
:to="{hash: heading.slug}"
>{{ heading.text }}</saber-link>
:href="{hash: heading.slug}"
>{{ heading.text }}</a>
</div>
</div>
</template>
Expand Down
8 changes: 4 additions & 4 deletions website/src/layouts/docs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
<slot name="default"/>
</div>
<div class="prev-next-page">
<saber-link class="prev-page" v-if="prevNextPage.prev" :to="prevNextPage.prev.link">
<a class="prev-page" v-if="prevNextPage.prev" :href="prevNextPage.prev.link">
<span class="arrow">←</span>
{{ prevNextPage.prev.title }}
</saber-link>
<saber-link class="next-page" v-if="prevNextPage.next" :to="prevNextPage.next.link">
</a>
<a class="next-page" v-if="prevNextPage.next" :href="prevNextPage.next.link">
{{ prevNextPage.next.title }}
<span class="arrow">→</span>
</saber-link>
</a>
</div>
</Wrap>
</template>
Expand Down

0 comments on commit 21ac524

Please sign in to comment.