Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat($markdown): TOC component (close: #1275) #1375

Merged
merged 1 commit into from
Mar 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/@vuepress/core/lib/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Content from './components/Content.js'
import ContentSlotsDistributor from './components/ContentSlotsDistributor'
import OutboundLink from './components/OutboundLink.vue'
import ClientOnly from './components/ClientOnly'
import TOC from './components/TOC.vue'

// suggest dev server restart on base change
if (module.hot) {
Expand Down Expand Up @@ -46,6 +47,8 @@ Vue.component('ClientOnly', ClientOnly)
// core components
Vue.component('Layout', getLayoutAsyncComponent('Layout'))
Vue.component('NotFound', getLayoutAsyncComponent('NotFound'))
// markdown components
Vue.component('TOC', TOC)

// global helper for adding base path to absolute urls
Vue.prototype.$withBase = function (path) {
Expand Down
20 changes: 20 additions & 0 deletions packages/@vuepress/core/lib/app/components/HeaderList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>
<component :is="listType[0]">
<li v-for="(item, index) in items" :key="index">
<router-link :to="'#' + item.slug" v-text="item.title" />
<HeaderList v-if="item.children" :items="item.children" :list-type="innerListType" />
</li>
</component>
</template>

<script>
export default {
name: 'HeaderList',
props: ['items', 'listType'],
computed: {
innerListType () {
return this.listType.slice(Math.min(this.listType.length - 1, 1))
}
}
}
</script>
57 changes: 57 additions & 0 deletions packages/@vuepress/core/lib/app/components/TOC.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<template>
<div>
<slot name="header" />
<HeaderList :items="groupedHeaders" :list-type="listTypes" />
<slot name="footer" />
</div>
</template>

<script>
import HeaderList from './HeaderList.vue'
export default {
props: {
listType: {
type: [String, Array],
default: 'ul'
},
includeLevel: {
type: Array,
default: () => [2, 3]
}
},
components: { HeaderList },
computed: {
listTypes () {
return typeof this.listType === 'string' ? [this.listType] : this.listType
},
groupedHeaders () {
return this.groupHeaders(this.$page.headers).list
}
},
methods: {
groupHeaders (headers, startLevel = 1) {
const list = []
let index = 0
while (index < headers.length) {
const header = headers[index]
if (header.level < startLevel) break
if (header.level > startLevel) {
const result = this.groupHeaders(headers.slice(index), header.level)
if (list.length) {
list[list.length - 1].children = result.list
} else {
list.push(...result.list)
}
index += result.index
} else {
if (header.level <= this.includeLevel[1] && header.level >= this.includeLevel[0]) {
list.push({ ...header })
}
index += 1
}
}
return { list, index }
}
}
}
</script>
10 changes: 3 additions & 7 deletions packages/@vuepress/markdown/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ const convertRouterLinkPlugin = require('./lib/link')
const containersPlugin = require('./lib/containers')
const markdownSlotsContainersPlugin = require('./lib/markdownSlotsContainers')
const snippetPlugin = require('./lib/snippet')
const tocPlugin = require('./lib/tableOfContents')
const emojiPlugin = require('markdown-it-emoji')
const anchorPlugin = require('markdown-it-anchor')
const tocPlugin = require('markdown-it-table-of-contents')
const { parseHeaders, slugify: _slugify, logger, chalk, hash } = require('@vuepress/shared-utils')
const { slugify: _slugify, logger, chalk, hash } = require('@vuepress/shared-utils')

/**
* Create markdown by config.
Expand Down Expand Up @@ -97,11 +97,7 @@ module.exports = (markdown = {}) => {
.end()

.plugin(PLUGINS.TOC)
.use(tocPlugin, [Object.assign({
slugify,
includeLevel: [2, 3],
format: parseHeaders
}, toc)])
.use(tocPlugin, [toc])
.end()

if (lineNumbers) {
Expand Down
83 changes: 83 additions & 0 deletions packages/@vuepress/markdown/lib/tableOfContents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// reference: https://github.com/Oktavilla/markdown-it-table-of-contents

const defaults = {
includeLevel: [2, 3],
containerClass: 'table-of-contents',
markerPattern: /^\[\[toc\]\]/im,
listType: 'ul',
containerHeaderHtml: '',
containerFooterHtml: ''
}

module.exports = (md, options) => {
options = Object.assign({}, defaults, options)
const tocRegexp = options.markerPattern

function toc (state, silent) {
var token
var match

// Reject if the token does not start with [
if (state.src.charCodeAt(state.pos) !== 0x5B /* [ */) {
return false
}
// Don't run any pairs in validation mode
if (silent) {
return false
}

// Detect TOC markdown
match = tocRegexp.exec(state.src)
match = !match ? [] : match.filter(function (m) { return m })
if (match.length < 1) {
return false
}

// Build content
token = state.push('toc_open', 'toc', 1)
token.markup = '[[toc]]'
token = state.push('toc_body', '', 0)
token = state.push('toc_close', 'toc', -1)

// Update pos so the parser can continue
var newline = state.src.indexOf('\n')
if (newline !== -1) {
state.pos = state.pos + newline
} else {
state.pos = state.pos + state.posMax + 1
}

return true
}

md.renderer.rules.toc_open = function () {
return vBindEscape`<TOC
:class=${options.containerClass}
:list-type=${options.listType}
:include-level=${options.includeLevel}
>`
}

md.renderer.rules.toc_body = function () {
return `<template slot="header">${options.containerHeaderHtml}</template>`
+ `<template slot="footer">${options.containerFooterHtml}</template>`
}

md.renderer.rules.toc_close = function () {
return `</TOC>`
}

// Insert TOC
md.inline.ruler.after('emphasis', 'toc', toc)
}

/** escape double quotes in v-bind derivatives */
function vBindEscape (strs, ...args) {
return strs.reduce((prev, curr, index) => {
return prev + curr + (index >= args.length
? ''
: `"${JSON.stringify(args[index])
.replace(/"/g, "'")
.replace(/([^\\])(\\\\)*\\'/g, (_, char) => char + '\\u0022')}"`)
}, '')
}
1 change: 0 additions & 1 deletion packages/@vuepress/markdown/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
"markdown-it-chain": "^1.3.0",
"markdown-it-container": "^2.0.0",
"markdown-it-emoji": "^1.4.0",
"markdown-it-table-of-contents": "^0.4.0",
"prismjs": "^1.13.0"
},
"author": "Evan You",
Expand Down
12 changes: 10 additions & 2 deletions packages/docs/docs/config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,17 @@ The key and value pair will be added to `<a>` tags that point to an external lin
### markdown.toc

- Type: `Object`
- Default: `{ includeLevel: [2, 3] }`

Options for [markdown-it-table-of-contents](https://github.com/Oktavilla/markdown-it-table-of-contents). (Note: prefer `markdown.slugify` if you want to customize header ids.)
This attribute will control the behaviour of `[[TOC]]`. It contains the following options:

- includeLevel: [number, number], level of headers to be included, defaults to `[2, 3]`.
- containerClass: string, the class name for the container, defaults to `table-of-contents`.
- markerPattern: RegExp, the regular expression for the marker to be replaced with TOC, defaults to `/^\[\[toc\]\]/im`.
- listType: string or Array, labels for all levels of the list, defaults to `"ul"`.
- containerHeaderHtml: string, an HTML string for container header, defaults to `""`.
- containerFooterHtml: string, an HTML string for container footer, defaults to `""`.

We also provide a [global component TOC](../guide/using-vue.md#toc) which allows for more free control by passing props directly to `<TOC>`.

### markdown.extendMarkdown

Expand Down
10 changes: 8 additions & 2 deletions packages/docs/docs/guide/markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,21 @@ A list of all emojis available can be found [here](https://github.com/markdown-i

**Input**

```
```md
[[toc]]
```

or

```md
<TOC/>
```

**Output**

[[toc]]

Rendering of TOC can be configured using the [`markdown.toc`](../config/README.md#markdown-toc) option.
Rendering of TOC can be configured using the [`markdown.toc`](../config/README.md#markdown-toc) option, or as props of [TOC component](./using-vue.md#toc), like `<TOC list-type="ol" :include-level="[2, Infinity]"/>`.

## Custom Containers

Expand Down
21 changes: 21 additions & 0 deletions packages/docs/docs/guide/using-vue.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,27 @@ Specify a specific slot for a specific page (.md) for rendering. This will be ve
- [Markdown Slot](./markdown-slot.md)
- [Writing a theme > Content Outlet](../theme/writing-a-theme.md#content-outlet)

### TOC <Badge text="1.0.0-alpha.41+"/>

- **Props**:
- `listType` - string or Array, defaults to `"ul"`
- `includeLevel` - [number, number], defaults to `[2, 3]`

- **Slots**: `header`, `footer`

- **Usage**:

You can add a custom table of contents by specify some props to this component. `includeLevel` decides which level of headers should be included. `listType` decides the tags of lists. If specified as an array, the component will take the first element as the first-level list type and so on. If there are not enough values provided, the last value will be used for all the remaining list types.

``` md
<TOC :list-type="['ol', 'ul']">
<p slot="header"><strong>Custom Table of Contents</strong></p>
</TOC>
```

<TOC :list-type="['ol', 'ul']">
<p slot="header"><strong>Custom Table of Contents</strong></p>
</TOC>

### Badge <Badge text="beta" type="warn"/> <Badge text="0.10.1+"/> <Badge text="default theme"/>

Expand Down
12 changes: 10 additions & 2 deletions packages/docs/docs/zh/config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,17 @@ VuePress 提供了一种添加额外样式的简便方法。你可以创建一
### markdown.toc

- 类型: `Object`
- 默认值: `{ includeLevel: [2, 3] }`

[markdown-it-table-of-contents](https://github.com/Oktavilla/markdown-it-table-of-contents) 的选项。
这个值将会控制 `[[TOC]]` 默认行为。它包含下面的选项:

- includeLevel: [number, number],决定哪些级别的标题会被显示在目录中,默认值为 `[2, 3]`。
- containerClass: string,决定了目录容器的类名,默认值为 `table-of-contents`。
- markerPattern: RegExp,决定了标题匹配的正则表达式,默认值为 `/^\[\[toc\]\]/im`。
- listType: string 或 Array,决定了各级列表的标签,默认值为 `"ul"`。
- containerHeaderHtml: string,在目录开头插入的 HTML 字符串,默认值为 `""`。
- containerFooterHtml: string,在目录结尾插入的 HTML 字符串,默认值为 `""`。

此外,我们还提供了[全局组件 TOC](../guide/using-vue.md#toc),可以通过直接向 `<TOC>` 传递属性实现更加自由的控制。

### markdown.extendMarkdown

Expand Down
10 changes: 8 additions & 2 deletions packages/docs/docs/zh/guide/markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,21 @@ lang: en-US

**Input**

```
```md
[[toc]]
```

或者

```md
<TOC/>
```

**Output**

[[toc]]

目录(Table of Contents)的渲染可以通过 [`markdown.toc`](../config/README.md#markdown-toc) 选项来配置。
目录(Table of Contents)的渲染可以通过 [`markdown.toc`](../config/README.md#markdown-toc) 选项来配置,也可以在 [TOC 组件](./using-vue.md#toc)中直接传入,如 `<TOC list-type="ol" :include-level="[2, Infinity]"/>`

## 自定义容器

Expand Down
21 changes: 21 additions & 0 deletions packages/docs/docs/zh/guide/using-vue.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,27 @@ export default {
- [Markdown 插槽](./markdown-slot.md)
- [开发主题 > 获取渲染内容](../theme/writing-a-theme.md#获取渲染内容)

### TOC <Badge text="1.0.0-alpha.41+"/>

- **Props**:
- `listType` - string 或 Array, 默认值为 `"ul"`
- `includeLevel` - [number, number], 默认值为 `[2, 3]`

- **Slots**: `header`, `footer`

- **Usage**:

你可以通过一些属性来实现一个自定义的目录。`includeLevel` 决定了哪些级别的标题会被显示在目录中。`listType` 决定了所有列表的标签。如果设置为了数组,组件将会使用第一个元素作为第一级列表的标签,以此类推。如果提供的标签不够多,将使用提供的最后一个值作为全部剩下的列表标签。

``` md
<TOC :list-type="['ol', 'ul']">
<p slot="header"><strong>自定义目录</strong></p>
</TOC>
```

<TOC :list-type="['ol', 'ul']">
<p slot="header"><strong>自定义目录</strong></p>
</TOC>

### Badge <Badge text="beta" type="warn"/> <Badge text="0.10.1+"/> <Badge text="默认主题"/>

Expand Down