From 62bfc75310056156b046d621167c998e8570b019 Mon Sep 17 00:00:00 2001 From: Chris Maas Date: Fri, 12 Nov 2021 11:46:05 +0100 Subject: [PATCH] 0.6.0 Rewrite of TOC generator The TOC generator was rewritten, because the old *on-the-fly* generator couldn't deal with unexpected order of headings and double-indentations. It is now a three-step process: 1. Gather all headings in a list. 2. Turn that list into a nested tree. 3. Generate HTML code based on the nested tree. Although all tests pass, this release could introduce some **breaking changes** for you, if you relied on the old way of doing things. Check the test cases to get a better understanding how this plugin handles various cases. * **Added**: Support for `markdown-it-attrs` (fixes #54) * **Changed**: Respects unexpected nesting order (fixes #55) * **Changed**: Uses anchor targets from existing id attributes (for example, set by `markdown-it-attrs` or `markdown-it-anchor`) * **Changed**: Now nests list correctly if there is a jump (for example: h2, h2, h4 -> h4 is now double-indented) * **Removed**: unused tests --- CHANGELOG.md | 25 ++ README.md | 112 +++++- index.js | 283 ++++++++++----- package-lock.json | 343 ++++++++++++++++++- package.json | 13 +- test/fixtures/custom-attrs-with-anchors.html | 7 + test/fixtures/custom-attrs.html | 7 + test/fixtures/custom-attrs.md | 13 + test/fixtures/full-example.html | 9 + test/fixtures/full-example.md | 17 + test/fixtures/full-toc-sample-result.html | 6 - test/fixtures/full-toc-sample.md | 8 - test/fixtures/multi-level-1234.html | 10 + test/fixtures/multi-level-23.html | 10 + test/fixtures/multi-level.md | 19 + test/fixtures/strange-order.html | 5 + test/fixtures/strange-order.md | 9 + test/modules/test.js | 104 +++++- 18 files changed, 881 insertions(+), 119 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 test/fixtures/custom-attrs-with-anchors.html create mode 100644 test/fixtures/custom-attrs.html create mode 100644 test/fixtures/custom-attrs.md create mode 100644 test/fixtures/full-example.html create mode 100644 test/fixtures/full-example.md delete mode 100644 test/fixtures/full-toc-sample-result.html delete mode 100644 test/fixtures/full-toc-sample.md create mode 100644 test/fixtures/multi-level-1234.html create mode 100644 test/fixtures/multi-level-23.html create mode 100644 test/fixtures/multi-level.md create mode 100644 test/fixtures/strange-order.html create mode 100644 test/fixtures/strange-order.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f34ca2b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +– + +## [0.6.0] - 2021-11-12 + +The TOC generator was rewritten, because the old *on-the-fly* generator couldn't deal with unexpected order of headings and double-indentations. It is now a three-step process: + +1. Gather all headings in a list. +2. Turn that list into a nested tree. +3. Generate HTML code based on the nested tree. + +Although all tests pass, this release could introduce some **breaking changes** for you, if you relied on the old way of doing things. Check the test cases to get a better understanding how this plugin handles various cases. + +* **Added**: Support for `markdown-it-attrs` (fixes #54) +* **Changed**: Respects unexpected nesting order (fixes #55) +* **Changed**: Uses anchor targets from existing id attributes (for example, set by `markdown-it-attrs` or `markdown-it-anchor`) +* **Changed**: Now nests list correctly if there is a jump (for example: h2, h2, h4 -> h4 is now double-indented) +* **Removed**: unused tests \ No newline at end of file diff --git a/README.md b/README.md index dd894d0..b9b96a4 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,13 @@ # markdown-it-table-of-contents A table of contents plugin for Markdown-it. Simple, customizable and with a default slugifier that matches that of https://www.npmjs.com/package/markdown-it-anchor (>5.0.0). -## Looking for maintainer -I'm looking for someone to take over this package to maintain and improve on it. Interested? Open an issue and just quickly explain your thoughts... - ## Usage ``` javascript var MarkdownIt = require("markdown-it"); var md = new MarkdownIt(); -md.use(require("markdown-it-anchor").default); // Optional, but makes sense as you really want to link to something +md.use(require("markdown-it-anchor").default); // Optional, but makes sense as you really want to link to something, see info about recommended plugins below md.use(require("markdown-it-table-of-contents")); ``` @@ -95,3 +92,110 @@ function transformLink(link) { return transformedLink; } ``` + +## Recommended plugins + +By default, markdown-it-table-of-contents collects all headings and renders a nested list. It uses the `slugify()` function to create anchor targets for the links in the list. However, the headlines in your markdown document are not touched by markdown-it-table-of-contents. You'd have a nice table of contents, but the links don't link to anything. That's why you need another plugin to generate ids (anchor link targets) for all of your headlines. There are two recommended plugins to achieve this: + +### [markdown-it-anchor](https://www.npmjs.com/package/markdown-it-anchor) + +This plugin transforms all headlines in a markdown document so that the HTML code includes an id. It *slugifies* the headline: + +```markdown +## Hello world, I think you should read this article +``` + +Becomes + +```html +

Hello world

+``` + +### [markdown-it-attrs](https://www.npmjs.com/package/markdown-it-attrs) + +This plugin lets you attach custom attributes to your headlines. This is especially useful, if you have long headlines but want short anchors: + +```markdown +## Hello world, I think you should read this article {#hello} +``` + +Becomes + +```html +

Hello world, I think you should read this article

+``` + +## Full example with unusual headline order + +Of course, both plugins can be combined. markdown-it-anchor ignores headlines that already have an id attribute. + +Furthermore, markdown-it-table-of-contents can handle unusual heading orders. Consider the full example below: + +```js +var md = new MarkdownIt(); +md.use(markdownItTOC, { + "includeLevel": [2,3,4] +}); +md.use(require("markdown-it-attrs")); +md.use(require("markdown-it-anchor")); +``` + + +```markdown +# Article + +[[toc]] + +### A message from our sponsors + +Ad + +## Hello world, I think you should read this article {#hello} + +Lorem ipsum + +## What's next? + +Read this next... + +#### See related articles {#related} +``` + +HTML output: + +```html +

Article

+

+

+ +
+

+

A message from our sponsors

+

Ad

+

Hello world, I think you should read this article

+

Lorem ipsum

+

What's next?

+

Read this next...

+ +``` + +## Additional infos + +* This plugin outputs a semantically correct table of contents. Sub-lists are rendered within the parent `
  • ` tag and not as a separate (empty) `
  • `. +* Headlines can be in an arbitrary order. For example, h3, h2, h4. Please note that the jump from h2 to h4 causes a doube-indentation, which is correct. \ No newline at end of file diff --git a/index.js b/index.js index c4640f2..39487fe 100644 --- a/index.js +++ b/index.js @@ -1,14 +1,25 @@ +//@ts-check 'use strict'; -var slugify = function(s){ - return encodeURIComponent(String(s).trim().toLowerCase().replace(/\s+/g, '-')) + +/* +* markdown-it-table-of-contents +* +* The algorithm works as follows: +* Step 1: Gather all headline tokens from a Markdown document and put them in an array. +* Step 2: Turn the flat array into a nested tree, respecting the correct headline level. +* Step 3: Turn the nested tree into HTML code. +*/ + +const slugify = function (s) { + return encodeURIComponent(String(s).trim().toLowerCase().replace(/\s+/g, '-')); }; -var defaults = { - includeLevel: [ 1, 2 ], +const defaults = { + includeLevel: [1, 2], containerClass: 'table-of-contents', slugify: slugify, markerPattern: /^\[\[toc\]\]/im, listType: 'ul', - format: function(content, md) { + format: function (content, md) { return md.renderInline(content); }, forceFullToc: false, @@ -17,19 +28,182 @@ var defaults = { transformLink: undefined, }; -module.exports = function(md, o) { - var options = Object.assign({}, defaults, o); - var tocRegexp = options.markerPattern; - var gstate; - var links; +/** +* @typedef {Object} HeadlineItem +* @property {number} level Headline level +* @property {string} anchor Anchor target +* @property {string} text Text of headline +*/ + +/** +* @typedef {Object} TocItem +* @property {number} level Item level +* @property {string} text Text of link +* @property {string} anchor Target of link +* @property {Array} children Sub-items for this list item +* @property {TocItem} parent Parent this item belongs to +*/ + +/** +* Finds all headline items for the defined levels in a Markdown document. +* @param {Array} levels includeLevels like `[1, 2, 3]` +* @param {*} tokens Tokens gathered by the plugin +* @param {*} options Plugin options +* @returns {Array} +*/ +function findHeadlineElements(levels, tokens, options) { + const headings = []; + let currentHeading = null; + + tokens.forEach(token => { + if (token.type === 'heading_open') { + const id = findExistingIdAttr(token); + const level = parseInt(token.tag.toLowerCase().replace('h', ''), 10); + if (levels.indexOf(level) >= 0) { + currentHeading = { + level: level, + text: null, + anchor: id || null + }; + } + } + else if (currentHeading && token.type === 'inline') { + const textContent = token.children + .filter((childToken) => childToken.type === 'text' || childToken.type === 'code_inline') + .reduce((acc, t) => acc + t.content, ''); + currentHeading.text = textContent; + if (! currentHeading.anchor) { + currentHeading.anchor = options.slugify(textContent, token.content); + } + } + else if (token.type === 'heading_close') { + if (currentHeading) { + headings.push(currentHeading); + } + currentHeading = null; + } + }); + + return headings; +} + +/** +* Helper to find an existing id attr on a token. Should be a heading_open token, but could be anything really +* Provided by markdown-it-anchor or markdown-it-attrs +* @param {any} token Token +* @returns {string} Id attribute to use as anchor +*/ +function findExistingIdAttr(token) { + if (token && token.attrs && token.attrs.length > 0) { + const idAttr = token.attrs.find( (attr) => { + if (Array.isArray(attr) && attr.length >= 2) { + return attr[0] === 'id'; + } + return false; + }); + if (idAttr && Array.isArray(idAttr) && idAttr.length >= 2) { + const [key, val] = idAttr; + return val; + } + } + return null; +} + +/** +* Helper to get minimum headline level so that the TOC is nested correctly +* @param {Array} headlineItems Search these +* @returns {number} Minimum level +*/ +function getMinLevel(headlineItems) { + return Math.min(...headlineItems.map(item => item.level)); +} + +/** +* Helper that creates a TOCItem +* @param {number} level +* @param {string} text +* @param {string} anchor +* @param {TocItem} rootNode +* @returns {TocItem} +*/ +function addListItem(level, text, anchor, rootNode) { + const listItem = { level, text, anchor, children: [], parent: rootNode }; + rootNode.children.push(listItem); + return listItem; +} + +/** +* Turns a list of flat headline items into a nested tree object representing the TOC +* @param {Array} headlineItems +* @returns {TocItem} Tree of TOC items +*/ +function flatHeadlineItemsToNestedTree(headlineItems) { + // create a root node with no text that holds the entire TOC. this won't be rendered, but only its children + const toc = { level: getMinLevel(headlineItems) - 1, anchor: null, text: null, children: [], parent: null }; + // pointer that tracks the last root item of the current list + let currentRootNode = toc; + // pointer that tracks the last item (to turn it into a new root node if necessary) + let prevListItem = currentRootNode; + + headlineItems.forEach(headlineItem => { + // if level is bigger, take the previous node, add a child list, set current list to this new child list + if (headlineItem.level > prevListItem.level) { + // eslint-disable-next-line no-unused-vars + Array.from({ length: headlineItem.level - prevListItem.level }).forEach(_ => { + currentRootNode = prevListItem; + prevListItem = addListItem(headlineItem.level, null, null, currentRootNode); + }); + prevListItem.text = headlineItem.text; + prevListItem.anchor = headlineItem.anchor; + } + // if level is same, add to the current list + else if (headlineItem.level === prevListItem.level) { + prevListItem = addListItem(headlineItem.level, headlineItem.text, headlineItem.anchor, currentRootNode); + } + // if level is smaller, set current list to currentlist.parent + else if (headlineItem.level < prevListItem.level) { + for (let i = 0; i < prevListItem.level - headlineItem.level; i++) { + currentRootNode = currentRootNode.parent; + } + prevListItem = addListItem(headlineItem.level, headlineItem.text, headlineItem.anchor, currentRootNode); + } + }); + + return toc; +} + +/** +* Recursively turns a nested tree of tocItems to HTML. +* @param {TocItem} tocItem +* @returns {string} +*/ +function tocItemToHtml(tocItem, options, md) { + return '<' + options.listType + '>' + tocItem.children.map(childItem => { + let li = '
  • '; + let anchor = childItem.anchor; + if (options && options.transformLink) { + anchor = options.transformLink(anchor); + } + + let text = childItem.text ? options.format(childItem.text, md, anchor) : null; + + li += anchor ? `${text}` : (text || ''); + + return li + (childItem.children.length > 0 ? tocItemToHtml(childItem, options, md) : '') + '
  • '; + }).join('') + ''; +} + +module.exports = function (md, o) { + const options = Object.assign({}, defaults, o); + const tocRegexp = options.markerPattern; + let gstate; function toc(state, silent) { - var token; - var match; - links = []; + let token; + let match; // Reject if the token does not start with [ - if (state.src.charCodeAt(state.pos) !== 0x5B /* [ */ ) { + if (state.src.charCodeAt(state.pos) !== 0x5B /* [ */) { return false; } // Don't run any pairs in validation mode @@ -39,7 +213,7 @@ module.exports = function(md, o) { // Detect TOC markdown match = tocRegexp.exec(state.src.substr(state.pos)); - match = !match ? [] : match.filter(function(m) { return m; }); + match = !match ? [] : match.filter(function (m) { return m; }); if (match.length < 1) { return false; } @@ -61,8 +235,8 @@ module.exports = function(md, o) { return true; } - md.renderer.rules.toc_open = function(tokens, index) { - var tocOpenHtml = '
    '; + md.renderer.rules.toc_open = function (tokens, index) { + var tocOpenHtml = '
    '; if (options.containerHeaderHtml) { tocOpenHtml += options.containerHeaderHtml; @@ -71,7 +245,7 @@ module.exports = function(md, o) { return tocOpenHtml; }; - md.renderer.rules.toc_close = function(tokens, index) { + md.renderer.rules.toc_close = function (tokens, index) { var tocFooterHtml = ''; if (options.containerFooterHtml) { @@ -81,78 +255,19 @@ module.exports = function(md, o) { return tocFooterHtml + '
    '; }; - md.renderer.rules.toc_body = function(tokens, index) { + md.renderer.rules.toc_body = function (tokens, index) { if (options.forceFullToc) { - throw("forceFullToc was removed in version 0.5.0. For more information, see https://github.com/Oktavilla/markdown-it-table-of-contents/pull/41") + throw ("forceFullToc was removed in version 0.5.0. For more information, see https://github.com/Oktavilla/markdown-it-table-of-contents/pull/41"); } else { - return renderChildsTokens(0, gstate.tokens)[1]; + const headlineItems = findHeadlineElements(options.includeLevel, gstate.tokens, options); + const toc = flatHeadlineItemsToNestedTree(headlineItems); + const html = tocItemToHtml(toc, options, md); + return html; } }; - function renderChildsTokens(pos, tokens) { - var headings = [], - buffer = '', - currentLevel, - subHeadings, - size = tokens.length, - i = pos; - while(i < size) { - var token = tokens[i]; - var heading = tokens[i - 1]; - var level = token.tag && parseInt(token.tag.substr(1, 1)); - if (token.type !== 'heading_close' || options.includeLevel.indexOf(level) == -1 || heading.type !== 'inline') { - i++; continue; // Skip if not matching criteria - } - if (!currentLevel) { - currentLevel = level;// We init with the first found level - } else { - if (level > currentLevel) { - subHeadings = renderChildsTokens(i, tokens); - buffer += subHeadings[1]; - i = subHeadings[0]; - continue; - } - if (level < currentLevel) { - // Finishing the sub headings - buffer += ""; - headings.push(buffer); - return [i, "<"+ options.listType +">"+ headings.join('') +""]; - } - if (level == currentLevel) { - // Finishing the sub headings - buffer += ""; - headings.push(buffer); - } - } - var content = heading.children - .filter((token) => token.type === 'text' || token.type === 'code_inline') - .reduce((acc, t) => acc + t.content, ''); - var slugifiedContent = options.slugify(content); - var link = "#"+slugifiedContent; - if (options.transformLink) { - link = options.transformLink(link); - } - - // Check if this link has been generated before and increment link if so - var generatedLink = link; - var index = 2; - while (links.indexOf(generatedLink) >= 0) { - generatedLink = link + "-" + index++; - } - links.push(generatedLink); - - buffer = `
  • `; - buffer += options.format(content, md, generatedLink); - buffer += ``; - i++; - } - buffer += buffer === '' ? '' : '
  • '; - headings.push(buffer); - return [i, "<"+ options.listType +">"+ headings.join('') +""]; - } - // Catch all the tokens for iteration later - md.core.ruler.push('grab_state', function(state) { + md.core.ruler.push('grab_state', function (state) { gstate = state; }); diff --git a/package-lock.json b/package-lock.json index 8af63fa..f85dfed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,8 +1,329 @@ { "name": "markdown-it-table-of-contents", - "version": "0.5.2", - "lockfileVersion": 1, + "version": "0.6.0", + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "markdown-it-table-of-contents", + "version": "0.6.0", + "license": "MIT", + "devDependencies": { + "markdown-it": "~8.4.2", + "markdown-it-anchor": "~5.0.1", + "markdown-it-attrs": "^2.4.1", + "mocha": "~5.2.0" + }, + "engines": { + "node": ">6.4.0" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/entities": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", + "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true, + "engines": { + "node": ">=4.x" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "node_modules/linkify-it": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.0.3.tgz", + "integrity": "sha1-2UpGSPmxwXnWT6lykSaL22zpQ08=", + "dev": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/markdown-it": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", + "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "entities": "~1.1.1", + "linkify-it": "^2.0.0", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it-anchor": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.0.2.tgz", + "integrity": "sha512-AFM/woBI8QDJMS/9+MmsBMT5/AR+ImfOsunQZTZhzcTmna3rIzAzbOh5E0l6mlFM/i9666BpUtkqQ9bS7WApCg==", + "dev": true, + "peerDependencies": { + "markdown-it": "^8.4.1" + } + }, + "node_modules/markdown-it-attrs": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/markdown-it-attrs/-/markdown-it-attrs-2.4.1.tgz", + "integrity": "sha512-BASnIYS+JLpjlhDf7jLV8VOuccxjfDDnQcz5dLfgPsYw8OsgbASexADdIkF7tIdGn+jaQSA4qOZXM3v3W3JBCg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "markdown-it": "^8.4.2" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "node_modules/mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dev": true, + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "dependencies": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "node_modules/supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/uc.micro": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.5.tgz", + "integrity": "sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + }, "dependencies": { "argparse": { "version": "1.0.10", @@ -138,23 +459,31 @@ } }, "markdown-it": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.1.0.tgz", - "integrity": "sha1-OJAtTnusImDAc+tnvmIyEfuywuM=", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", + "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", "dev": true, "requires": { "argparse": "^1.0.7", "entities": "~1.1.1", "linkify-it": "^2.0.0", "mdurl": "^1.0.1", - "uc.micro": "^1.0.3" + "uc.micro": "^1.0.5" } }, "markdown-it-anchor": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.0.2.tgz", "integrity": "sha512-AFM/woBI8QDJMS/9+MmsBMT5/AR+ImfOsunQZTZhzcTmna3rIzAzbOh5E0l6mlFM/i9666BpUtkqQ9bS7WApCg==", - "dev": true + "dev": true, + "requires": {} + }, + "markdown-it-attrs": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/markdown-it-attrs/-/markdown-it-attrs-2.4.1.tgz", + "integrity": "sha512-BASnIYS+JLpjlhDf7jLV8VOuccxjfDDnQcz5dLfgPsYw8OsgbASexADdIkF7tIdGn+jaQSA4qOZXM3v3W3JBCg==", + "dev": true, + "requires": {} }, "mdurl": { "version": "1.0.1", diff --git a/package.json b/package.json index 2d0a363..85e87e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "license": "MIT", - "version": "0.5.2", + "version": "0.6.0", "name": "markdown-it-table-of-contents", "main": "index.js", "scripts": { @@ -26,10 +26,13 @@ "url": "https://github.com/martinlissmyr/markdown-it-table-of-contents/issues" }, "author": "Martin Lissmyr ", - "contributors": "Listed at ", + "contributors": [ + "Listed at " + ], "devDependencies": { - "mocha": "~5.2.0", - "markdown-it": "~8.1.0", - "markdown-it-anchor": "~5.0.1" + "markdown-it": "~8.4.2", + "markdown-it-anchor": "~5.0.1", + "markdown-it-attrs": "^2.4.1", + "mocha": "~5.2.0" } } diff --git a/test/fixtures/custom-attrs-with-anchors.html b/test/fixtures/custom-attrs-with-anchors.html new file mode 100644 index 0000000..051a542 --- /dev/null +++ b/test/fixtures/custom-attrs-with-anchors.html @@ -0,0 +1,7 @@ +

    Article

    +

    Using custom heading id attributes via markdown-it-attrs.

    +

    +

    Welcome to the show

    +

    Note the anchor targets for TOC links.

    +

    This has no custom id

    +

    The headline above falls back to the slugify function as anchor target.

    diff --git a/test/fixtures/custom-attrs.html b/test/fixtures/custom-attrs.html new file mode 100644 index 0000000..051a542 --- /dev/null +++ b/test/fixtures/custom-attrs.html @@ -0,0 +1,7 @@ +

    Article

    +

    Using custom heading id attributes via markdown-it-attrs.

    +

    +

    Welcome to the show

    +

    Note the anchor targets for TOC links.

    +

    This has no custom id

    +

    The headline above falls back to the slugify function as anchor target.

    diff --git a/test/fixtures/custom-attrs.md b/test/fixtures/custom-attrs.md new file mode 100644 index 0000000..87eceb6 --- /dev/null +++ b/test/fixtures/custom-attrs.md @@ -0,0 +1,13 @@ +# Article {#my-article} + +Using custom heading id attributes via markdown-it-attrs. + +[[toc]] + +## Welcome to the show {#section-1} + +Note the anchor targets for TOC links. + +## This has no custom id + +The headline above falls back to the slugify function as anchor target. diff --git a/test/fixtures/full-example.html b/test/fixtures/full-example.html new file mode 100644 index 0000000..192d7cf --- /dev/null +++ b/test/fixtures/full-example.html @@ -0,0 +1,9 @@ +

    Article

    +

    +

    A message from our sponsors

    +

    Ad

    +

    Hello world, I think you should read this article

    +

    Lorem ipsum

    +

    What's next?

    +

    Read this next...

    + diff --git a/test/fixtures/full-example.md b/test/fixtures/full-example.md new file mode 100644 index 0000000..cdbf529 --- /dev/null +++ b/test/fixtures/full-example.md @@ -0,0 +1,17 @@ +# Article + +[[toc]] + +### A message from our sponsors + +Ad + +## Hello world, I think you should read this article {#hello} + +Lorem ipsum + +## What's next? + +Read this next... + +#### See related articles {#related} \ No newline at end of file diff --git a/test/fixtures/full-toc-sample-result.html b/test/fixtures/full-toc-sample-result.html deleted file mode 100644 index 3a03332..0000000 --- a/test/fixtures/full-toc-sample-result.html +++ /dev/null @@ -1,6 +0,0 @@ -

    Some text with soft break before toc
    -

    -

    Heading 2

    -

    Some nice text

    -

    Heading 1

    -

    Some text

    diff --git a/test/fixtures/full-toc-sample.md b/test/fixtures/full-toc-sample.md deleted file mode 100644 index 71425f1..0000000 --- a/test/fixtures/full-toc-sample.md +++ /dev/null @@ -1,8 +0,0 @@ -Some text with soft break before toc -[[toc]] - -## Heading 2 -Some nice text - -# Heading 1 -Some text diff --git a/test/fixtures/multi-level-1234.html b/test/fixtures/multi-level-1234.html new file mode 100644 index 0000000..6a8b6e3 --- /dev/null +++ b/test/fixtures/multi-level-1234.html @@ -0,0 +1,10 @@ +

    Article

    +

    +

    B

    +

    C

    +

    D

    +

    E

    +

    F

    +

    G

    +

    Next headline is two levels deeper

    +

    H

    diff --git a/test/fixtures/multi-level-23.html b/test/fixtures/multi-level-23.html new file mode 100644 index 0000000..ddaaf98 --- /dev/null +++ b/test/fixtures/multi-level-23.html @@ -0,0 +1,10 @@ +

    Article

    +

    +

    B

    +

    C

    +

    D

    +

    E

    +

    F

    +

    G

    +

    Next headline is two levels deeper

    +

    H

    diff --git a/test/fixtures/multi-level.md b/test/fixtures/multi-level.md new file mode 100644 index 0000000..7c3cac0 --- /dev/null +++ b/test/fixtures/multi-level.md @@ -0,0 +1,19 @@ +# Article + +[[toc]] + +## B + +## C + +### D + +### E + +## F + +## G + +Next headline is two levels deeper + +#### H diff --git a/test/fixtures/strange-order.html b/test/fixtures/strange-order.html new file mode 100644 index 0000000..aaf9c62 --- /dev/null +++ b/test/fixtures/strange-order.html @@ -0,0 +1,5 @@ +

    X

    +

    +

    A

    +

    B

    +

    C

    diff --git a/test/fixtures/strange-order.md b/test/fixtures/strange-order.md new file mode 100644 index 0000000..87896b5 --- /dev/null +++ b/test/fixtures/strange-order.md @@ -0,0 +1,9 @@ +### X + +[[toc]] + +# A + +### B + +## C diff --git a/test/modules/test.js b/test/modules/test.js index 2842162..5a089d2 100644 --- a/test/modules/test.js +++ b/test/modules/test.js @@ -3,6 +3,7 @@ var assert = require("assert"); var fs = require("fs"); var MarkdownIt = require("markdown-it"); var markdownItAnchor = require("markdown-it-anchor"); +var markdownItAttrs = require("markdown-it-attrs"); var markdownItTOC = require("../../index"); // Defaults @@ -25,10 +26,22 @@ var simpleWithDuplicateHeadings = fs.readFileSync("test/fixtures/simple-with-dup var simpleWithDuplicateHeadingsHTML = fs.readFileSync("test/fixtures/simple-with-duplicate-headings.html", "utf-8"); var emptyMarkdown = defaultMarker; var emptyMarkdownHtml = fs.readFileSync("test/fixtures/empty.html", "utf-8"); -var fullTocSampleMarkdown = fs.readFileSync("test/fixtures/full-toc-sample.md", "utf-8"); -var fullTocSampleHtml = fs.readFileSync("test/fixtures/full-toc-sample-result.html", "utf-8"); -const slugify = (s) => encodeURIComponent(String(s).trim().toLowerCase().replace(/\s+/g, '-')); +var multiLevelMarkdown = fs.readFileSync("test/fixtures/multi-level.md", "utf-8"); +var multiLevel1234HTML = fs.readFileSync("test/fixtures/multi-level-1234.html", "utf-8"); +var multiLevel23HTML = fs.readFileSync("test/fixtures/multi-level-23.html", "utf-8"); +var strangeOrderMarkdown = fs.readFileSync("test/fixtures/strange-order.md", "utf-8"); +var strangeOrderHTML = fs.readFileSync("test/fixtures/strange-order.html", "utf-8"); + +var customAttrsMarkdown = fs.readFileSync("test/fixtures/custom-attrs.md", "utf-8"); +var customAttrsHTML = fs.readFileSync("test/fixtures/custom-attrs.html", "utf-8"); +var customAttrsWithAnchorsHTML = fs.readFileSync("test/fixtures/custom-attrs-with-anchors.html", "utf-8"); + +var fullExampleMarkdown = fs.readFileSync("test/fixtures/full-example.md", "utf-8"); +var fullExampleHTML = fs.readFileSync("test/fixtures/full-example.html", "utf-8"); + + +var slugify = (s) => encodeURIComponent(String(s).trim().toLowerCase().replace(/\s+/g, '-')); var endOfLine = require('os').EOL; @@ -41,15 +54,15 @@ function adjustEOL(text) { } describe("Testing Markdown rendering", function() { - var md = new MarkdownIt(); - it("Parses correctly with default settings", function(done) { + var md = new MarkdownIt(); md.use(markdownItTOC); assert.equal(adjustEOL(md.render(simpleMarkdown)), simpleDefaultHTML); done(); }); it("Parses correctly with includeLevel set", function(done) { + var md = new MarkdownIt(); md.use(markdownItTOC, { "includeLevel": [2] }); @@ -58,6 +71,7 @@ describe("Testing Markdown rendering", function() { }); it("Parses correctly with containerClass set", function(done) { + var md = new MarkdownIt(); var customContainerClass = "custom-container-class"; md.use(markdownItTOC, { "containerClass": customContainerClass @@ -67,6 +81,7 @@ describe("Testing Markdown rendering", function() { }); it("Parses correctly with markerPattern set", function(done) { + var md = new MarkdownIt(); var customMarker = "[[custom-marker]]"; md.use(markdownItTOC, { "markerPattern": /^\[\[custom-marker\]\]/im @@ -76,6 +91,7 @@ describe("Testing Markdown rendering", function() { }); it("Parses correctly with listType set", function(done) { + var md = new MarkdownIt(); var customListType = "ol"; md.use(markdownItTOC, { "listType": customListType @@ -85,12 +101,14 @@ describe("Testing Markdown rendering", function() { }); it("Formats markdown by default", function(done) { + var md = new MarkdownIt(); md.use(markdownItTOC); assert.equal(adjustEOL(md.render(simpleWithFormatting)), simpleWithFormattingHTML); done(); }); it("Parses correctly with custom formatting", function(done) { + var md = new MarkdownIt(); var customHeading = "Heading with custom formatting 123abc"; md.use(markdownItTOC, { format: function(str) { return customHeading; } @@ -100,6 +118,7 @@ describe("Testing Markdown rendering", function() { }); it("Custom formatting includes markdown and link", function(done) { + var md = new MarkdownIt(); md.use(markdownItTOC, { format: function(str, md, link) { assert.ok(MarkdownIt.prototype.isPrototypeOf(md)); @@ -112,6 +131,7 @@ describe("Testing Markdown rendering", function() { }); it("Slugs matches markdown-it-anchor", function(done) { + var md = new MarkdownIt(); md.use(markdownItAnchor); md.use(markdownItTOC); assert.equal(adjustEOL(md.render(simpleMarkdown)), simpleWithAnchorsHTML); @@ -119,12 +139,16 @@ describe("Testing Markdown rendering", function() { }); it("Generates empty TOC", function(done) { + var md = new MarkdownIt(); + md.use(markdownItAnchor); md.use(markdownItTOC); assert.equal(adjustEOL(md.render(emptyMarkdown)), emptyMarkdownHtml); done(); }); it("Throws an error if forceFullToc is enabled", function (done) { + var md = new MarkdownIt(); + md.use(markdownItAnchor); md.use(markdownItTOC, { forceFullToc: true }); @@ -133,6 +157,8 @@ describe("Testing Markdown rendering", function() { }); it("Parses correctly with container header and footer html set", function (done) { + var md = new MarkdownIt(); + md.use(markdownItAnchor); md.use(markdownItTOC, { slugify, @@ -144,6 +170,8 @@ describe("Testing Markdown rendering", function() { }); it("Generates TOC, with custom transformed link", function (done) { + var md = new MarkdownIt(); + md.use(markdownItAnchor); md.use(markdownItTOC, { slugify, @@ -156,16 +184,82 @@ describe("Testing Markdown rendering", function() { }); it("Parses correctly when headers are links", function (done) { + var md = new MarkdownIt(); md.use(markdownItTOC); + md.use(markdownItAnchor); assert.equal(adjustEOL(md.render(simpleWithHeadingLink)), simpleWithHeadingLinkHTML); done(); }); it("Parses correctly with duplicate headers", function (done) { + var md = new MarkdownIt(); md.use(markdownItTOC, { "includeLevel": [1,2,3,4] }); + md.use(markdownItAnchor); assert.equal(adjustEOL(md.render(simpleWithDuplicateHeadings)), simpleWithDuplicateHeadingsHTML); done(); }); + + it("Parses correctly with multiple levels", function(done) { + var md = new MarkdownIt(); + //md.use(markdownItAnchor); + md.use(markdownItTOC, { + "includeLevel": [1, 2, 3, 4] + }); + assert.equal(adjustEOL(md.render(multiLevelMarkdown)), multiLevel1234HTML); + done(); + }); + + it("Parses correctly with subset of multiple levels", function(done) { + var md = new MarkdownIt(); + //md.use(markdownItAnchor); + md.use(markdownItTOC, { + "includeLevel": [2, 3] + }); + assert.equal(adjustEOL(md.render(multiLevelMarkdown)), multiLevel23HTML); + done(); + }); + + it("Can manage headlines in a strange order", function(done) { + var md = new MarkdownIt(); + //md.use(markdownItAnchor); + md.use(markdownItTOC, { + "includeLevel": [1, 2, 3] + }); + assert.equal(adjustEOL(md.render(strangeOrderMarkdown)), strangeOrderHTML); + done(); + }); + + it("Parses correctly with custom heading id attrs", function (done) { + var md = new MarkdownIt(); + md.use(markdownItTOC, { + "includeLevel": [1,2,3,4] + }); + md.use(markdownItAttrs); + assert.equal(adjustEOL(md.render(customAttrsMarkdown)), customAttrsHTML); + done(); + }); + + it("Parses correctly when combining markdown-it-attrs and markdown-it-anchor", function (done) { + var md = new MarkdownIt(); + md.use(markdownItTOC, { + "includeLevel": [1,2,3,4] + }); + md.use(markdownItAttrs); + assert.equal(adjustEOL(md.render(customAttrsMarkdown)), customAttrsWithAnchorsHTML); + done(); + }); + + it("Full example", function (done) { + var md = new MarkdownIt(); + md.use(markdownItTOC, { + "includeLevel": [2,3,4] + }); + md.use(markdownItAttrs); + md.use(markdownItAnchor); + assert.equal(adjustEOL(md.render(fullExampleMarkdown)), fullExampleHTML); + done(); + }); + });