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

fix($markdown): invalid toc links #2210

Closed
wants to merge 6 commits into from
Closed
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
Original file line number Diff line number Diff line change
@@ -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 id="h1"><a class="header-anchor" href="#h1" aria-hidden="true">#</a> H1</h1>
<p></p>
<div class="table-of-contents">
<ul>
<li><a href="#h2">H2</a>
<ul>
<li><a href="#h3">H3 😄</a></li>
</ul>
</li>
<li><a href="#h2-2">H2</a>
<ul>
<li><a href="#h3-2">H3
<Badge /></a></li>
</ul>
</li>
</ul>
</div>
<p></p>
<h2 id="h2"><a class="header-anchor" href="#h2" aria-hidden="true">#</a> H2</h2>
<h3 id="h3"><a class="header-anchor" href="#h3" aria-hidden="true">#</a> H3 😄</h3>
<h2 id="h2-2"><a class="header-anchor" href="#h2-2" aria-hidden="true">#</a> H2</h2>
<h3 id="h3-2"><a class="header-anchor" href="#h3-2" aria-hidden="true">#</a> H3
<Badge />
</h3>
`;
11 changes: 11 additions & 0 deletions packages/@vuepress/markdown/__tests__/fragments/toc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# H1

[[toc]]

## H2

### H3 :smile:

## H2

### H3 <Badge />
24 changes: 24 additions & 0 deletions packages/@vuepress/markdown/__tests__/tableOfContents.spec.js
Original file line number Diff line number Diff line change
@@ -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()
})
})
9 changes: 2 additions & 7 deletions packages/@vuepress/markdown/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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) {
Expand Down
141 changes: 141 additions & 0 deletions packages/@vuepress/markdown/lib/tableOfContents.js
Original file line number Diff line number Diff line change
@@ -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 = `</p><div class="${options.containerClass}">`

if (options.containerHeaderHtml) {
tocOpenHtml += options.containerHeaderHtml
}

return tocOpenHtml
}

md.renderer.rules.toc_close = function () {
var tocFooterHtml = ''

if (options.containerFooterHtml) {
tocFooterHtml = options.containerFooterHtml
}

return tocFooterHtml + `</div><p>`
}

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 + '</li>')
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 = `<li><a href="${link}">`
element += typeof options.format === 'function' ? options.format(content) : content
element += `</a></li>`
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)
})
}
1 change: 0 additions & 1 deletion packages/@vuepress/markdown/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
4 changes: 0 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down