From 760f90b914907001e48732e1823aa4a464ba2869 Mon Sep 17 00:00:00 2001 From: Shigma <33423008+Shigma@users.noreply.github.com> Date: Sun, 3 Mar 2019 00:40:31 +0800 Subject: [PATCH] feat($markdown): TOC component (close: #1275) (#1375) --- packages/@vuepress/core/lib/app/app.js | 3 + .../core/lib/app/components/HeaderList.vue | 20 +++++ .../@vuepress/core/lib/app/components/TOC.vue | 57 +++++++++++++ packages/@vuepress/markdown/index.js | 10 +-- .../@vuepress/markdown/lib/tableOfContents.js | 83 +++++++++++++++++++ packages/@vuepress/markdown/package.json | 1 - packages/docs/docs/config/README.md | 12 ++- packages/docs/docs/guide/markdown.md | 10 ++- packages/docs/docs/guide/using-vue.md | 21 +++++ packages/docs/docs/zh/config/README.md | 12 ++- packages/docs/docs/zh/guide/markdown.md | 10 ++- packages/docs/docs/zh/guide/using-vue.md | 21 +++++ 12 files changed, 244 insertions(+), 16 deletions(-) create mode 100644 packages/@vuepress/core/lib/app/components/HeaderList.vue create mode 100644 packages/@vuepress/core/lib/app/components/TOC.vue create mode 100644 packages/@vuepress/markdown/lib/tableOfContents.js diff --git a/packages/@vuepress/core/lib/app/app.js b/packages/@vuepress/core/lib/app/app.js index 70bc09683f..bcccec1797 100644 --- a/packages/@vuepress/core/lib/app/app.js +++ b/packages/@vuepress/core/lib/app/app.js @@ -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) { @@ -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) { diff --git a/packages/@vuepress/core/lib/app/components/HeaderList.vue b/packages/@vuepress/core/lib/app/components/HeaderList.vue new file mode 100644 index 0000000000..a1945e082c --- /dev/null +++ b/packages/@vuepress/core/lib/app/components/HeaderList.vue @@ -0,0 +1,20 @@ + + + diff --git a/packages/@vuepress/core/lib/app/components/TOC.vue b/packages/@vuepress/core/lib/app/components/TOC.vue new file mode 100644 index 0000000000..697e68c146 --- /dev/null +++ b/packages/@vuepress/core/lib/app/components/TOC.vue @@ -0,0 +1,57 @@ + + + diff --git a/packages/@vuepress/markdown/index.js b/packages/@vuepress/markdown/index.js index 60af7ca865..58c6a2a19c 100644 --- a/packages/@vuepress/markdown/index.js +++ b/packages/@vuepress/markdown/index.js @@ -16,10 +16,10 @@ const hoistScriptStylePlugin = require('./lib/hoist') const convertRouterLinkPlugin = require('./lib/link') 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. @@ -92,11 +92,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) { diff --git a/packages/@vuepress/markdown/lib/tableOfContents.js b/packages/@vuepress/markdown/lib/tableOfContents.js new file mode 100644 index 0000000000..5e32161ec7 --- /dev/null +++ b/packages/@vuepress/markdown/lib/tableOfContents.js @@ -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`` + } + + md.renderer.rules.toc_body = function () { + return `` + + `` + } + + md.renderer.rules.toc_close = function () { + return `` + } + + // 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')}"`) + }, '') +} diff --git a/packages/@vuepress/markdown/package.json b/packages/@vuepress/markdown/package.json index 14607ebad2..1f4309f644 100644 --- a/packages/@vuepress/markdown/package.json +++ b/packages/@vuepress/markdown/package.json @@ -25,7 +25,6 @@ "markdown-it-anchor": "^5.0.2", "markdown-it-chain": "^1.3.0", "markdown-it-emoji": "^1.4.0", - "markdown-it-table-of-contents": "^0.4.0", "prismjs": "^1.13.0" }, "author": "Evan You", diff --git a/packages/docs/docs/config/README.md b/packages/docs/docs/config/README.md index 19f394de65..914531940b 100644 --- a/packages/docs/docs/config/README.md +++ b/packages/docs/docs/config/README.md @@ -220,9 +220,17 @@ The key and value pair will be added to `` 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 ``. ### markdown.extendMarkdown diff --git a/packages/docs/docs/guide/markdown.md b/packages/docs/docs/guide/markdown.md index 8631f91531..868046ae4a 100644 --- a/packages/docs/docs/guide/markdown.md +++ b/packages/docs/docs/guide/markdown.md @@ -108,15 +108,21 @@ A list of all emojis available can be found [here](https://github.com/markdown-i **Input** -``` +```md [[toc]] ``` +or + +```md + +``` + **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 ``. ## Custom Containers diff --git a/packages/docs/docs/guide/using-vue.md b/packages/docs/docs/guide/using-vue.md index f5628b6a3a..52d46eb898 100644 --- a/packages/docs/docs/guide/using-vue.md +++ b/packages/docs/docs/guide/using-vue.md @@ -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 + +- **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 + +

Custom Table of Contents

+
+``` + + +

Custom Table of Contents

+
### Badge diff --git a/packages/docs/docs/zh/config/README.md b/packages/docs/docs/zh/config/README.md index c5042d356a..21e1bc17c7 100644 --- a/packages/docs/docs/zh/config/README.md +++ b/packages/docs/docs/zh/config/README.md @@ -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),可以通过直接向 `` 传递属性实现更加自由的控制。 ### markdown.extendMarkdown diff --git a/packages/docs/docs/zh/guide/markdown.md b/packages/docs/docs/zh/guide/markdown.md index e44611335a..db05999fd6 100644 --- a/packages/docs/docs/zh/guide/markdown.md +++ b/packages/docs/docs/zh/guide/markdown.md @@ -106,15 +106,21 @@ lang: en-US **Input** -``` +```md [[toc]] ``` +或者 + +```md + +``` + **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)中直接传入,如 ``。 ## 自定义容器 diff --git a/packages/docs/docs/zh/guide/using-vue.md b/packages/docs/docs/zh/guide/using-vue.md index cf6d8b2f22..57b758c3f2 100644 --- a/packages/docs/docs/zh/guide/using-vue.md +++ b/packages/docs/docs/zh/guide/using-vue.md @@ -225,6 +225,27 @@ export default { - [Markdown 插槽](./markdown-slot.md) - [开发主题 > 获取渲染内容](../theme/writing-a-theme.md#获取渲染内容) +### TOC + +- **Props**: + - `listType` - string 或 Array, 默认值为 `"ul"` + - `includeLevel` - [number, number], 默认值为 `[2, 3]` + +- **Slots**: `header`, `footer` + +- **Usage**: + +你可以通过一些属性来实现一个自定义的目录。`includeLevel` 决定了哪些级别的标题会被显示在目录中。`listType` 决定了所有列表的标签。如果设置为了数组,组件将会使用第一个元素作为第一级列表的标签,以此类推。如果提供的标签不够多,将使用提供的最后一个值作为全部剩下的列表标签。 + +``` md + +

自定义目录

+
+``` + + +

自定义目录

+
### Badge