diff --git a/packages/@vuepress/markdown/__tests__/__snapshots__/tableOfContents.spec.js.snap b/packages/@vuepress/markdown/__tests__/__snapshots__/tableOfContents.spec.js.snap
new file mode 100644
index 0000000000..6846ab71db
--- /dev/null
+++ b/packages/@vuepress/markdown/__tests__/__snapshots__/tableOfContents.spec.js.snap
@@ -0,0 +1,28 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`tableOfContents should generate unique and valid links with html and emoji 1`] = `
+
H1
+
+
+
+ H2
+ H3 😄
+ H2
+ H3
+
+
+`;
diff --git a/packages/@vuepress/markdown/__tests__/fragments/toc.md b/packages/@vuepress/markdown/__tests__/fragments/toc.md
new file mode 100644
index 0000000000..4afe4be1e7
--- /dev/null
+++ b/packages/@vuepress/markdown/__tests__/fragments/toc.md
@@ -0,0 +1,11 @@
+# H1
+
+[[toc]]
+
+## H2
+
+### H3 :smile:
+
+## H2
+
+### H3
diff --git a/packages/@vuepress/markdown/__tests__/tableOfContents.spec.js b/packages/@vuepress/markdown/__tests__/tableOfContents.spec.js
new file mode 100644
index 0000000000..9a510fbb94
--- /dev/null
+++ b/packages/@vuepress/markdown/__tests__/tableOfContents.spec.js
@@ -0,0 +1,24 @@
+import { getFragment } from '@vuepress/test-utils'
+import { Md } from './util'
+import emoji from 'markdown-it-emoji'
+import anchor from 'markdown-it-anchor'
+import toc from '../lib/tableOfContents'
+import slugify from '../../shared-utils/lib/slugify.js'
+
+const md = Md()
+ .set({ html: true })
+ .use(emoji)
+ .use(anchor, {
+ slugify,
+ permalink: true,
+ permalinkBefore: true,
+ permalinkSymbol: '#'
+ }).use(toc)
+
+describe('tableOfContents', () => {
+ test('should generate unique and valid links with html and emoji', () => {
+ const input = getFragment(__dirname, 'toc.md')
+ const output = md.render(input)
+ expect(output).toMatchSnapshot()
+ })
+})
diff --git a/packages/@vuepress/markdown/index.js b/packages/@vuepress/markdown/index.js
index fae5948ab4..d9f3ba4c78 100644
--- a/packages/@vuepress/markdown/index.js
+++ b/packages/@vuepress/markdown/index.js
@@ -14,12 +14,11 @@ const componentPlugin = require('./lib/component')
const hoistScriptStylePlugin = require('./lib/hoist')
const convertRouterLinkPlugin = require('./lib/link')
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 {
slugify: _slugify,
- parseHeaders,
logger, chalk, normalizeConfig,
moduleResolver: { getMarkdownItResolver }
} = require('@vuepress/shared-utils')
@@ -94,11 +93,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..cdfa7a2067
--- /dev/null
+++ b/packages/@vuepress/markdown/lib/tableOfContents.js
@@ -0,0 +1,141 @@
+// Reference: https://github.com/Oktavilla/markdown-it-table-of-contents
+const { slugify, parseHeaders } = require('@vuepress/shared-utils')
+
+const defaults = {
+ includeLevel: [2, 3],
+ containerClass: 'table-of-contents',
+ slugify,
+ markerPattern: /^\[\[toc\]\]/im,
+ listType: 'ul',
+ format: parseHeaders,
+ forceFullToc: false,
+ containerHeaderHtml: undefined,
+ containerFooterHtml: undefined,
+ transformLink: undefined
+}
+
+module.exports = (md, options) => {
+ options = Object.assign({}, defaults, options)
+ var gStateTokens
+
+ // Insert TOC rules after emphasis
+ md.inline.ruler.after('emphasis', 'toc', toc)
+
+ function toc (state, silent) {
+ /**
+ * Reject if
+ * 1. in validation mode
+ * 2. token does not start with [
+ * 3. it's not [[toc]]
+ */
+ if (silent // validation mode
+ || state.src.charCodeAt(state.pos) !== 0x5B /* [ */
+ || !options.markerPattern.test(state.src.substr(state.pos))) {
+ return false
+ }
+
+ // Build content
+ state.push('toc_open', 'toc', 1)
+ state.push('toc_body', '', 0)
+ state.push('toc_close', 'toc', -1)
+
+ // Update pos so the parser can continue
+ const newline = state.src.indexOf('\n', state.pos)
+
+ state.pos = newline !== -1
+ ? newline
+ : state.pos + state.posMax + 1
+
+ return true
+ }
+
+ md.renderer.rules.toc_open = function () {
+ var tocOpenHtml = ``
+
+ if (options.containerHeaderHtml) {
+ tocOpenHtml += options.containerHeaderHtml
+ }
+
+ return tocOpenHtml
+ }
+
+ md.renderer.rules.toc_close = function () {
+ var tocFooterHtml = ''
+
+ if (options.containerFooterHtml) {
+ tocFooterHtml = options.containerFooterHtml
+ }
+
+ return tocFooterHtml + `
`
+ }
+
+ md.renderer.rules.toc_body = function () {
+ if (options.forceFullToc) {
+ /*
+
+ Renders full TOC even if the hierarchy of headers contains
+ a header greater than the first appearing header
+
+ ## heading 2
+ ### heading 3
+ # heading 1
+
+ Result TOC:
+ - heading 2
+ - heading 3
+ - heading 1
+ */
+ let tocBody = ''
+
+ for (let pos = 0; pos < gStateTokens.length;) {
+ const [nextPos, subBody] = renderChildrenTokens(pos, gStateTokens)
+ pos = nextPos
+ tocBody += subBody
+ }
+
+ return tocBody
+ } else {
+ return renderChildrenTokens(0, gStateTokens)[1]
+ }
+ }
+
+ function renderChildrenTokens (pos, tokens) {
+ const headings = []
+ for (let i = pos, currentLevel; i < tokens.length; i++) {
+ const level = tokens[i].tag && parseInt(tokens[i].tag.substr(1, 1))
+ if (tokens[i].type === 'heading_close' && options.includeLevel.indexOf(level) > -1 && tokens[i - 1].type === 'inline') {
+ // init currentLevel at first round
+ currentLevel = currentLevel || level
+
+ if (level > currentLevel) {
+ const [nextPos, subHeadings] = renderChildrenTokens(i, tokens)
+ i = nextPos - 1
+ // nest ul into parent li
+ const last = headings.pop()
+ headings.push(last.slice(0, last.length - 5))
+ headings.push(subHeadings + '')
+ continue
+ } else if (level < currentLevel) {
+ return [i, `<${options.listType}>${headings.join('')}${options.listType}>`]
+ }
+
+ // get content from previous inline token
+ const content = tokens[i - 1].content
+ // instead of slugify the content directly, try to find id created by markdown-it-anchor first
+ let link = '#' + (tokens[i - 2].attrGet('id') || options.slugify(content))
+ link = typeof options.transformLink === 'function' ? options.transformLink(link) : link
+
+ let element = `
`
+ element += typeof options.format === 'function' ? options.format(content) : content
+ element += ``
+ headings.push(element)
+ }
+ }
+ return [tokens.length, `<${options.listType}>${headings.join('')}${options.listType}>`]
+ }
+
+ // Catch all the tokens for iteration later
+ md.core.ruler.push('grab_state', state => {
+ gStateTokens = state.tokens.slice(0)
+ })
+}
diff --git a/packages/@vuepress/markdown/package.json b/packages/@vuepress/markdown/package.json
index d9ef64aefc..32ed63b9c6 100644
--- a/packages/@vuepress/markdown/package.json
+++ b/packages/@vuepress/markdown/package.json
@@ -27,7 +27,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"
},
"devDependencies": {
diff --git a/yarn.lock b/yarn.lock
index ab0d6c343d..b8fcd5e1a2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8779,10 +8779,6 @@ markdown-it-emoji@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz#9bee0e9a990a963ba96df6980c4fddb05dfb4dcc"
-markdown-it-table-of-contents@^0.4.0:
- version "0.4.4"
- resolved "https://registry.yarnpkg.com/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.4.4.tgz#3dc7ce8b8fc17e5981c77cc398d1782319f37fbc"
-
markdown-it@^8.4.1:
version "8.4.2"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.2.tgz#386f98998dc15a37722aa7722084f4020bdd9b54"