Skip to content

Commit 760f90b

Browse files
shigmaulivz
authored andcommitted
feat($markdown): TOC component (close: #1275) (#1375)
1 parent 204cbe4 commit 760f90b

File tree

12 files changed

+244
-16
lines changed

12 files changed

+244
-16
lines changed

packages/@vuepress/core/lib/app/app.js

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import Content from './components/Content.js'
1616
import ContentSlotsDistributor from './components/ContentSlotsDistributor'
1717
import OutboundLink from './components/OutboundLink.vue'
1818
import ClientOnly from './components/ClientOnly'
19+
import TOC from './components/TOC.vue'
1920

2021
// suggest dev server restart on base change
2122
if (module.hot) {
@@ -46,6 +47,8 @@ Vue.component('ClientOnly', ClientOnly)
4647
// core components
4748
Vue.component('Layout', getLayoutAsyncComponent('Layout'))
4849
Vue.component('NotFound', getLayoutAsyncComponent('NotFound'))
50+
// markdown components
51+
Vue.component('TOC', TOC)
4952

5053
// global helper for adding base path to absolute urls
5154
Vue.prototype.$withBase = function (path) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<template>
2+
<component :is="listType[0]">
3+
<li v-for="(item, index) in items" :key="index">
4+
<router-link :to="'#' + item.slug" v-text="item.title" />
5+
<HeaderList v-if="item.children" :items="item.children" :list-type="innerListType" />
6+
</li>
7+
</component>
8+
</template>
9+
10+
<script>
11+
export default {
12+
name: 'HeaderList',
13+
props: ['items', 'listType'],
14+
computed: {
15+
innerListType () {
16+
return this.listType.slice(Math.min(this.listType.length - 1, 1))
17+
}
18+
}
19+
}
20+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<template>
2+
<div>
3+
<slot name="header" />
4+
<HeaderList :items="groupedHeaders" :list-type="listTypes" />
5+
<slot name="footer" />
6+
</div>
7+
</template>
8+
9+
<script>
10+
import HeaderList from './HeaderList.vue'
11+
export default {
12+
props: {
13+
listType: {
14+
type: [String, Array],
15+
default: 'ul'
16+
},
17+
includeLevel: {
18+
type: Array,
19+
default: () => [2, 3]
20+
}
21+
},
22+
components: { HeaderList },
23+
computed: {
24+
listTypes () {
25+
return typeof this.listType === 'string' ? [this.listType] : this.listType
26+
},
27+
groupedHeaders () {
28+
return this.groupHeaders(this.$page.headers).list
29+
}
30+
},
31+
methods: {
32+
groupHeaders (headers, startLevel = 1) {
33+
const list = []
34+
let index = 0
35+
while (index < headers.length) {
36+
const header = headers[index]
37+
if (header.level < startLevel) break
38+
if (header.level > startLevel) {
39+
const result = this.groupHeaders(headers.slice(index), header.level)
40+
if (list.length) {
41+
list[list.length - 1].children = result.list
42+
} else {
43+
list.push(...result.list)
44+
}
45+
index += result.index
46+
} else {
47+
if (header.level <= this.includeLevel[1] && header.level >= this.includeLevel[0]) {
48+
list.push({ ...header })
49+
}
50+
index += 1
51+
}
52+
}
53+
return { list, index }
54+
}
55+
}
56+
}
57+
</script>

packages/@vuepress/markdown/index.js

+3-7
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ const hoistScriptStylePlugin = require('./lib/hoist')
1616
const convertRouterLinkPlugin = require('./lib/link')
1717
const markdownSlotsContainersPlugin = require('./lib/markdownSlotsContainers')
1818
const snippetPlugin = require('./lib/snippet')
19+
const tocPlugin = require('./lib/tableOfContents')
1920
const emojiPlugin = require('markdown-it-emoji')
2021
const anchorPlugin = require('markdown-it-anchor')
21-
const tocPlugin = require('markdown-it-table-of-contents')
22-
const { parseHeaders, slugify: _slugify, logger, chalk, hash } = require('@vuepress/shared-utils')
22+
const { slugify: _slugify, logger, chalk, hash } = require('@vuepress/shared-utils')
2323

2424
/**
2525
* Create markdown by config.
@@ -92,11 +92,7 @@ module.exports = (markdown = {}) => {
9292
.end()
9393

9494
.plugin(PLUGINS.TOC)
95-
.use(tocPlugin, [Object.assign({
96-
slugify,
97-
includeLevel: [2, 3],
98-
format: parseHeaders
99-
}, toc)])
95+
.use(tocPlugin, [toc])
10096
.end()
10197

10298
if (lineNumbers) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// reference: https://github.com/Oktavilla/markdown-it-table-of-contents
2+
3+
const defaults = {
4+
includeLevel: [2, 3],
5+
containerClass: 'table-of-contents',
6+
markerPattern: /^\[\[toc\]\]/im,
7+
listType: 'ul',
8+
containerHeaderHtml: '',
9+
containerFooterHtml: ''
10+
}
11+
12+
module.exports = (md, options) => {
13+
options = Object.assign({}, defaults, options)
14+
const tocRegexp = options.markerPattern
15+
16+
function toc (state, silent) {
17+
var token
18+
var match
19+
20+
// Reject if the token does not start with [
21+
if (state.src.charCodeAt(state.pos) !== 0x5B /* [ */) {
22+
return false
23+
}
24+
// Don't run any pairs in validation mode
25+
if (silent) {
26+
return false
27+
}
28+
29+
// Detect TOC markdown
30+
match = tocRegexp.exec(state.src)
31+
match = !match ? [] : match.filter(function (m) { return m })
32+
if (match.length < 1) {
33+
return false
34+
}
35+
36+
// Build content
37+
token = state.push('toc_open', 'toc', 1)
38+
token.markup = '[[toc]]'
39+
token = state.push('toc_body', '', 0)
40+
token = state.push('toc_close', 'toc', -1)
41+
42+
// Update pos so the parser can continue
43+
var newline = state.src.indexOf('\n')
44+
if (newline !== -1) {
45+
state.pos = state.pos + newline
46+
} else {
47+
state.pos = state.pos + state.posMax + 1
48+
}
49+
50+
return true
51+
}
52+
53+
md.renderer.rules.toc_open = function () {
54+
return vBindEscape`<TOC
55+
:class=${options.containerClass}
56+
:list-type=${options.listType}
57+
:include-level=${options.includeLevel}
58+
>`
59+
}
60+
61+
md.renderer.rules.toc_body = function () {
62+
return `<template slot="header">${options.containerHeaderHtml}</template>`
63+
+ `<template slot="footer">${options.containerFooterHtml}</template>`
64+
}
65+
66+
md.renderer.rules.toc_close = function () {
67+
return `</TOC>`
68+
}
69+
70+
// Insert TOC
71+
md.inline.ruler.after('emphasis', 'toc', toc)
72+
}
73+
74+
/** escape double quotes in v-bind derivatives */
75+
function vBindEscape (strs, ...args) {
76+
return strs.reduce((prev, curr, index) => {
77+
return prev + curr + (index >= args.length
78+
? ''
79+
: `"${JSON.stringify(args[index])
80+
.replace(/"/g, "'")
81+
.replace(/([^\\])(\\\\)*\\'/g, (_, char) => char + '\\u0022')}"`)
82+
}, '')
83+
}

packages/@vuepress/markdown/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
"markdown-it-anchor": "^5.0.2",
2626
"markdown-it-chain": "^1.3.0",
2727
"markdown-it-emoji": "^1.4.0",
28-
"markdown-it-table-of-contents": "^0.4.0",
2928
"prismjs": "^1.13.0"
3029
},
3130
"author": "Evan You",

packages/docs/docs/config/README.md

+10-2
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,17 @@ The key and value pair will be added to `<a>` tags that point to an external lin
220220
### markdown.toc
221221

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

225-
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.)
224+
This attribute will control the behaviour of `[[TOC]]`. It contains the following options:
225+
226+
- includeLevel: [number, number], level of headers to be included, defaults to `[2, 3]`.
227+
- containerClass: string, the class name for the container, defaults to `table-of-contents`.
228+
- markerPattern: RegExp, the regular expression for the marker to be replaced with TOC, defaults to `/^\[\[toc\]\]/im`.
229+
- listType: string or Array, labels for all levels of the list, defaults to `"ul"`.
230+
- containerHeaderHtml: string, an HTML string for container header, defaults to `""`.
231+
- containerFooterHtml: string, an HTML string for container footer, defaults to `""`.
232+
233+
We also provide a [global component TOC](../guide/using-vue.md#toc) which allows for more free control by passing props directly to `<TOC>`.
226234

227235
### markdown.extendMarkdown
228236

packages/docs/docs/guide/markdown.md

+8-2
Original file line numberDiff line numberDiff line change
@@ -108,15 +108,21 @@ A list of all emojis available can be found [here](https://github.com/markdown-i
108108

109109
**Input**
110110

111-
```
111+
```md
112112
[[toc]]
113113
```
114114

115+
or
116+
117+
```md
118+
<TOC/>
119+
```
120+
115121
**Output**
116122

117123
[[toc]]
118124

119-
Rendering of TOC can be configured using the [`markdown.toc`](../config/README.md#markdown-toc) option.
125+
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]"/>`.
120126

121127
## Custom Containers
122128

packages/docs/docs/guide/using-vue.md

+21
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,27 @@ Specify a specific slot for a specific page (.md) for rendering. This will be ve
224224
- [Markdown Slot](./markdown-slot.md)
225225
- [Writing a theme > Content Outlet](../theme/writing-a-theme.md#content-outlet)
226226

227+
### TOC <Badge text="1.0.0-alpha.41+"/>
228+
229+
- **Props**:
230+
- `listType` - string or Array, defaults to `"ul"`
231+
- `includeLevel` - [number, number], defaults to `[2, 3]`
232+
233+
- **Slots**: `header`, `footer`
234+
235+
- **Usage**:
236+
237+
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.
238+
239+
``` md
240+
<TOC :list-type="['ol', 'ul']">
241+
<p slot="header"><strong>Custom Table of Contents</strong></p>
242+
</TOC>
243+
```
244+
245+
<TOC :list-type="['ol', 'ul']">
246+
<p slot="header"><strong>Custom Table of Contents</strong></p>
247+
</TOC>
227248

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

packages/docs/docs/zh/config/README.md

+10-2
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,17 @@ VuePress 提供了一种添加额外样式的简便方法。你可以创建一
212212
### markdown.toc
213213

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

217-
[markdown-it-table-of-contents](https://github.com/Oktavilla/markdown-it-table-of-contents) 的选项。
216+
这个值将会控制 `[[TOC]]` 默认行为。它包含下面的选项:
217+
218+
- includeLevel: [number, number],决定哪些级别的标题会被显示在目录中,默认值为 `[2, 3]`
219+
- containerClass: string,决定了目录容器的类名,默认值为 `table-of-contents`
220+
- markerPattern: RegExp,决定了标题匹配的正则表达式,默认值为 `/^\[\[toc\]\]/im`
221+
- listType: string 或 Array,决定了各级列表的标签,默认值为 `"ul"`
222+
- containerHeaderHtml: string,在目录开头插入的 HTML 字符串,默认值为 `""`
223+
- containerFooterHtml: string,在目录结尾插入的 HTML 字符串,默认值为 `""`
224+
225+
此外,我们还提供了[全局组件 TOC](../guide/using-vue.md#toc),可以通过直接向 `<TOC>` 传递属性实现更加自由的控制。
218226

219227
### markdown.extendMarkdown
220228

packages/docs/docs/zh/guide/markdown.md

+8-2
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,21 @@ lang: en-US
106106

107107
**Input**
108108

109-
```
109+
```md
110110
[[toc]]
111111
```
112112

113+
或者
114+
115+
```md
116+
<TOC/>
117+
```
118+
113119
**Output**
114120

115121
[[toc]]
116122

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

119125
## 自定义容器
120126

packages/docs/docs/zh/guide/using-vue.md

+21
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,27 @@ export default {
225225
- [Markdown 插槽](./markdown-slot.md)
226226
- [开发主题 > 获取渲染内容](../theme/writing-a-theme.md#获取渲染内容)
227227

228+
### TOC <Badge text="1.0.0-alpha.41+"/>
229+
230+
- **Props**:
231+
- `listType` - string 或 Array, 默认值为 `"ul"`
232+
- `includeLevel` - [number, number], 默认值为 `[2, 3]`
233+
234+
- **Slots**: `header`, `footer`
235+
236+
- **Usage**:
237+
238+
你可以通过一些属性来实现一个自定义的目录。`includeLevel` 决定了哪些级别的标题会被显示在目录中。`listType` 决定了所有列表的标签。如果设置为了数组,组件将会使用第一个元素作为第一级列表的标签,以此类推。如果提供的标签不够多,将使用提供的最后一个值作为全部剩下的列表标签。
239+
240+
``` md
241+
<TOC :list-type="['ol', 'ul']">
242+
<p slot="header"><strong>自定义目录</strong></p>
243+
</TOC>
244+
```
245+
246+
<TOC :list-type="['ol', 'ul']">
247+
<p slot="header"><strong>自定义目录</strong></p>
248+
</TOC>
228249

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

0 commit comments

Comments
 (0)