From c8baed6f5e7ea89e3f34cc13e2b757146853fd82 Mon Sep 17 00:00:00 2001 From: Alexey Pyltsyn Date: Sat, 13 Feb 2021 04:21:44 +0300 Subject: [PATCH] feat(v2): add ability to set custom heading id --- packages/docusaurus-mdx-loader/src/index.js | 4 +- .../__tests__/index.test.js | 64 +++++++++++++++++-- .../src/remark/{slug => headings}/index.js | 28 ++++++-- website/docs/guides/docs/docs-create-doc.mdx | 4 ++ .../src/pages/examples/markdownPageExample.md | 2 + 5 files changed, 92 insertions(+), 10 deletions(-) rename packages/docusaurus-mdx-loader/src/remark/{slug => headings}/__tests__/index.test.js (81%) rename packages/docusaurus-mdx-loader/src/remark/{slug => headings}/index.js (56%) diff --git a/packages/docusaurus-mdx-loader/src/index.js b/packages/docusaurus-mdx-loader/src/index.js index 5023a09e77167..ec5a01cb5797c 100644 --- a/packages/docusaurus-mdx-loader/src/index.js +++ b/packages/docusaurus-mdx-loader/src/index.js @@ -11,14 +11,14 @@ const mdx = require('@mdx-js/mdx'); const emoji = require('remark-emoji'); const matter = require('gray-matter'); const stringifyObject = require('stringify-object'); -const slug = require('./remark/slug'); +const headings = require('./remark/headings'); const toc = require('./remark/toc'); const transformImage = require('./remark/transformImage'); const transformLinks = require('./remark/transformLinks'); const DEFAULT_OPTIONS = { rehypePlugins: [], - remarkPlugins: [emoji, slug, toc], + remarkPlugins: [emoji, headings, toc], }; module.exports = async function docusaurusMdxLoader(fileString) { diff --git a/packages/docusaurus-mdx-loader/src/remark/slug/__tests__/index.test.js b/packages/docusaurus-mdx-loader/src/remark/headings/__tests__/index.test.js similarity index 81% rename from packages/docusaurus-mdx-loader/src/remark/slug/__tests__/index.test.js rename to packages/docusaurus-mdx-loader/src/remark/headings/__tests__/index.test.js index 887f897d709a3..d9331a4c11672 100644 --- a/packages/docusaurus-mdx-loader/src/remark/slug/__tests__/index.test.js +++ b/packages/docusaurus-mdx-loader/src/remark/headings/__tests__/index.test.js @@ -5,13 +5,15 @@ * LICENSE file in the root directory of this source tree. */ -/* Based on remark-slug (https://github.com/remarkjs/remark-slug) */ +/* Based on remark-slug (https://github.com/remarkjs/remark-slug) and gatsby-remark-autolink-headers (https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-remark-autolink-headers) */ /* eslint-disable no-param-reassign */ import remark from 'remark'; import u from 'unist-builder'; import removePosition from 'unist-util-remove-position'; +import toString from 'mdast-util-to-string'; +import visit from 'unist-util-visit'; import slug from '../index'; function process(doc, plugins = []) { @@ -27,7 +29,7 @@ function heading(label, id) { ); } -describe('slug plugin', () => { +describe('headings plugin', () => { test('should patch `id`s and `data.hProperties.id', () => { const result = process('# Normal\n\n## Table of Contents\n\n# Baz\n'); const expected = u('root', [ @@ -157,7 +159,7 @@ describe('slug plugin', () => { expect(result).toEqual(expected); }); - test('should create GitHub slugs', () => { + test('should create GitHub-style headings ids', () => { const result = process( [ '## I ♥ unicode', @@ -225,7 +227,7 @@ describe('slug plugin', () => { expect(result).toEqual(expected); }); - test('should generate slug from only text contents of headings if they contains HTML tags', () => { + test('should generate id from only text contents of headings if they contains HTML tags', () => { const result = process('# Normal\n'); const expected = u('root', [ u( @@ -244,4 +246,58 @@ describe('slug plugin', () => { expect(result).toEqual(expected); }); + + test('should create custom headings ids', () => { + const result = process(` +# Heading One {#custom_h1} + +## Heading Two {#custom-heading-two} + +# With *Bold* {#custom-withbold} + +# Snake-cased ID {#this_is_custom_id} + +# No custom ID + +# {#id-only} + +# {#text-after} custom ID + `); + + const headers = []; + visit(result, 'heading', (node) => { + headers.push({text: toString(node), id: node.data.id}); + }); + + expect(headers).toEqual([ + { + id: 'custom_h1', + text: 'Heading One', + }, + { + id: 'custom-heading-two', + text: 'Heading Two', + }, + { + id: 'custom-withbold', + text: 'With Bold', + }, + { + id: 'this_is_custom_id', + text: 'Snake-cased ID', + }, + { + id: 'no-custom-id', + text: 'No custom I', + }, + { + id: 'id-only', + text: '{#id-only}', + }, + { + id: 'text-after-custom-id', + text: '{#text-after} custom ID', + }, + ]); + }); }); diff --git a/packages/docusaurus-mdx-loader/src/remark/slug/index.js b/packages/docusaurus-mdx-loader/src/remark/headings/index.js similarity index 56% rename from packages/docusaurus-mdx-loader/src/remark/slug/index.js rename to packages/docusaurus-mdx-loader/src/remark/headings/index.js index ad8cb51f88e1c..1fad54ee8e3c4 100644 --- a/packages/docusaurus-mdx-loader/src/remark/slug/index.js +++ b/packages/docusaurus-mdx-loader/src/remark/headings/index.js @@ -5,12 +5,14 @@ * LICENSE file in the root directory of this source tree. */ -/* Based on remark-slug (https://github.com/remarkjs/remark-slug) */ +/* Based on remark-slug (https://github.com/remarkjs/remark-slug) and gatsby-remark-autolink-headers (https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-remark-autolink-headers) */ const visit = require('unist-util-visit'); const toString = require('mdast-util-to-string'); const slugs = require('github-slugger')(); +const customHeadingIdRegex = /^(.*?)\s*\{#([\w-]+)\}$/; + function slug() { const transformer = (ast) => { slugs.reset(); @@ -26,11 +28,29 @@ function slug() { const headingTextNodes = headingNode.children.filter( ({type}) => !['html', 'jsx'].includes(type), ); - const normalizedHeadingNode = + const heading = toString( headingTextNodes.length > 0 ? {children: headingTextNodes} - : headingNode; - id = slugs.slug(toString(normalizedHeadingNode)); + : headingNode, + ); + + // Support explicit heading IDs + const customHeadingIdMatches = customHeadingIdRegex.exec(heading); + + if (customHeadingIdMatches) { + id = customHeadingIdMatches[2]; + + // Remove the custom ID part from the text node + if (headingNode.children.length > 1) { + headingNode.children.pop(); + } else { + const lastNode = + headingNode.children[headingNode.children.length - 1]; + lastNode.value = customHeadingIdMatches[1] || heading; + } + } else { + id = slugs.slug(heading); + } } data.id = id; diff --git a/website/docs/guides/docs/docs-create-doc.mdx b/website/docs/guides/docs/docs-create-doc.mdx index b1856efcd46c8..49841e715820d 100644 --- a/website/docs/guides/docs/docs-create-doc.mdx +++ b/website/docs/guides/docs/docs-create-doc.mdx @@ -44,6 +44,10 @@ The headers are well-spaced so that the hierarchy is clear. - that you want your users to remember - and you may nest them - multiple times + +### Custom id headers {#custom-id} + +With `{#custom-id}` syntax you can set your own header id. ``` This will render in the browser as follows: diff --git a/website/src/pages/examples/markdownPageExample.md b/website/src/pages/examples/markdownPageExample.md index 1c9e1722de901..3e9b539256a7d 100644 --- a/website/src/pages/examples/markdownPageExample.md +++ b/website/src/pages/examples/markdownPageExample.md @@ -119,3 +119,5 @@ import MyComponentSource from '!!raw-loader!@site/src/pages/examples/\_myCompone {MyComponentSource} + +## Custom heading id {#custom}