diff --git a/packages/docusaurus-mdx-loader/package.json b/packages/docusaurus-mdx-loader/package.json
index 6bb336d4f2..091a22adfe 100644
--- a/packages/docusaurus-mdx-loader/package.json
+++ b/packages/docusaurus-mdx-loader/package.json
@@ -19,14 +19,15 @@
"loader-utils": "^1.2.3",
"mdast-util-to-string": "^1.0.7",
"remark-emoji": "^2.0.2",
- "remark-slug": "^5.1.2",
"stringify-object": "^3.3.0",
"unist-util-visit": "^2.0.1"
},
"devDependencies": {
"remark": "^11.0.2",
"remark-mdx": "^1.5.1",
- "to-vfile": "^6.0.0"
+ "to-vfile": "^6.0.0",
+ "unist-builder": "^2.0.3",
+ "unist-util-remove-position": "^2.0.1"
},
"engines": {
"node": ">=10.9.0"
diff --git a/packages/docusaurus-mdx-loader/src/index.js b/packages/docusaurus-mdx-loader/src/index.js
index f89e34af90..55a78a2ba4 100644
--- a/packages/docusaurus-mdx-loader/src/index.js
+++ b/packages/docusaurus-mdx-loader/src/index.js
@@ -9,9 +9,9 @@ const {getOptions} = require('loader-utils');
const {readFile} = require('fs-extra');
const mdx = require('@mdx-js/mdx');
const emoji = require('remark-emoji');
-const slug = require('remark-slug');
const matter = require('gray-matter');
const stringifyObject = require('stringify-object');
+const slug = require('./remark/slug');
const rightToc = require('./remark/rightToc');
const DEFAULT_OPTIONS = {
diff --git a/packages/docusaurus-mdx-loader/src/remark/rightToc/__tests__/__snapshots__/index.test.js.snap b/packages/docusaurus-mdx-loader/src/remark/rightToc/__tests__/__snapshots__/index.test.js.snap
index 56cfc085d5..bdc25c3df4 100644
--- a/packages/docusaurus-mdx-loader/src/remark/rightToc/__tests__/__snapshots__/index.test.js.snap
+++ b/packages/docusaurus-mdx-loader/src/remark/rightToc/__tests__/__snapshots__/index.test.js.snap
@@ -62,7 +62,7 @@ exports[`non text phrasing content 1`] = `
},
{
value: 'HTML',
- id: 'ihtmli',
+ id: 'html',
children: []
},
{
diff --git a/packages/docusaurus-mdx-loader/src/remark/rightToc/__tests__/index.test.js b/packages/docusaurus-mdx-loader/src/remark/rightToc/__tests__/index.test.js
index 85f7106b47..c5931ff5bb 100644
--- a/packages/docusaurus-mdx-loader/src/remark/rightToc/__tests__/index.test.js
+++ b/packages/docusaurus-mdx-loader/src/remark/rightToc/__tests__/index.test.js
@@ -10,11 +10,13 @@ import remark from 'remark';
import mdx from 'remark-mdx';
import vfile from 'to-vfile';
import plugin from '../index';
+import slug from '../../slug/index';
const processFixture = async (name, options) => {
const path = join(__dirname, 'fixtures', `${name}.md`);
const file = await vfile.read(path);
const result = await remark()
+ .use(slug)
.use(mdx)
.use(plugin, options)
.process(file);
diff --git a/packages/docusaurus-mdx-loader/src/remark/rightToc/search.js b/packages/docusaurus-mdx-loader/src/remark/rightToc/search.js
index 2619ac6def..5c89b314a2 100644
--- a/packages/docusaurus-mdx-loader/src/remark/rightToc/search.js
+++ b/packages/docusaurus-mdx-loader/src/remark/rightToc/search.js
@@ -8,7 +8,6 @@
const toString = require('mdast-util-to-string');
const visit = require('unist-util-visit');
const escapeHtml = require('escape-html');
-const slugs = require('github-slugger')();
// https://github.com/syntax-tree/mdast#heading
function toValue(node) {
@@ -40,19 +39,18 @@ function search(node) {
let current = -1;
let currentDepth = 0;
- slugs.reset();
-
const onHeading = (child, index, parent) => {
const value = toString(child);
- const id =
- child.data && child.data.hProperties && child.data.hProperties.id;
- const slug = slugs.slug(id || value);
if (parent !== node || !value || child.depth > 3 || child.depth < 2) {
return;
}
- const entry = {value: toValue(child), id: slug, children: []};
+ const entry = {
+ value: toValue(child),
+ id: child.data.id,
+ children: [],
+ };
if (!headings.length || currentDepth >= child.depth) {
headings.push(entry);
diff --git a/packages/docusaurus-mdx-loader/src/remark/slug/__tests__/index.test.js b/packages/docusaurus-mdx-loader/src/remark/slug/__tests__/index.test.js
new file mode 100644
index 0000000000..5070253260
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/slug/__tests__/index.test.js
@@ -0,0 +1,247 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/* Based on remark-slug (https://github.com/remarkjs/remark-slug) */
+
+/* eslint-disable no-param-reassign */
+
+import remark from 'remark';
+import u from 'unist-builder';
+import removePosition from 'unist-util-remove-position';
+import slug from '../index';
+
+function process(doc, plugins = []) {
+ const processor = remark().use({plugins: [...plugins, slug]});
+ return removePosition(processor.runSync(processor.parse(doc)), true);
+}
+
+function heading(label, id) {
+ return u(
+ 'heading',
+ {depth: 2, data: {id, hProperties: {id}}},
+ label ? [u('text', label)] : [],
+ );
+}
+
+describe('slug 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', [
+ u(
+ 'heading',
+ {depth: 1, data: {hProperties: {id: 'normal'}, id: 'normal'}},
+ [u('text', 'Normal')],
+ ),
+ u(
+ 'heading',
+ {
+ depth: 2,
+ data: {
+ hProperties: {id: 'table-of-contents'},
+ id: 'table-of-contents',
+ },
+ },
+ [u('text', 'Table of Contents')],
+ ),
+ u('heading', {depth: 1, data: {hProperties: {id: 'baz'}, id: 'baz'}}, [
+ u('text', 'Baz'),
+ ]),
+ ]);
+
+ expect(result).toEqual(expected);
+ });
+
+ test('should not overwrite `data` on headings', () => {
+ const result = process('# Normal\n', [
+ function() {
+ function transform(tree) {
+ tree.children[0].data = {foo: 'bar'};
+ }
+ return transform;
+ },
+ ]);
+ const expected = u('root', [
+ u(
+ 'heading',
+ {
+ depth: 1,
+ data: {foo: 'bar', hProperties: {id: 'normal'}, id: 'normal'},
+ },
+ [u('text', 'Normal')],
+ ),
+ ]);
+
+ expect(result).toEqual(expected);
+ });
+
+ test('should not overwrite `data.hProperties` on headings', () => {
+ const result = process('# Normal\n', [
+ function() {
+ function transform(tree) {
+ tree.children[0].data = {hProperties: {className: ['foo']}};
+ }
+ return transform;
+ },
+ ]);
+ const expected = u('root', [
+ u(
+ 'heading',
+ {
+ depth: 1,
+ data: {hProperties: {className: ['foo'], id: 'normal'}, id: 'normal'},
+ },
+ [u('text', 'Normal')],
+ ),
+ ]);
+
+ expect(result).toEqual(expected);
+ });
+
+ test('should generate `id`s and `hProperties.id`s, based on `hProperties.id` if they exist', () => {
+ const result = process(
+ [
+ '## Something',
+ '## Something here',
+ '## Something there',
+ '## Something also',
+ ].join('\n\n'),
+ [
+ function() {
+ function transform(tree) {
+ tree.children[1].data = {hProperties: {id: 'here'}};
+ tree.children[3].data = {hProperties: {id: 'something'}};
+ }
+ return transform;
+ },
+ ],
+ );
+ const expected = u('root', [
+ u(
+ 'heading',
+ {
+ depth: 2,
+ data: {hProperties: {id: 'something'}, id: 'something'},
+ },
+ [u('text', 'Something')],
+ ),
+ u(
+ 'heading',
+ {
+ depth: 2,
+ data: {hProperties: {id: 'here'}, id: 'here'},
+ },
+ [u('text', 'Something here')],
+ ),
+ u(
+ 'heading',
+ {
+ depth: 2,
+ data: {hProperties: {id: 'something-there'}, id: 'something-there'},
+ },
+ [u('text', 'Something there')],
+ ),
+ u(
+ 'heading',
+ {
+ depth: 2,
+ data: {hProperties: {id: 'something-1'}, id: 'something-1'},
+ },
+ [u('text', 'Something also')],
+ ),
+ ]);
+
+ expect(result).toEqual(expected);
+ });
+
+ test('should create GitHub slugs', () => {
+ const result = process(
+ [
+ '## I โฅ unicode',
+ '',
+ '## Dash-dash',
+ '',
+ '## enโdash',
+ '',
+ '## emโdash',
+ '',
+ '## ๐ unicode emoji',
+ '',
+ '## ๐-๐ unicode emoji',
+ '',
+ '## ๐_๐ unicode emoji',
+ '',
+ '##',
+ '',
+ '## ',
+ '',
+ '## Initial spaces',
+ '',
+ '## Final spaces ',
+ '',
+ '## Duplicate',
+ '',
+ '## Duplicate',
+ '',
+ '## :ok: No underscore',
+ '',
+ '## :ok_hand: Single',
+ '',
+ '## :ok_hand::hatched_chick: Two in a row with no spaces',
+ '',
+ '## :ok_hand: :hatched_chick: Two in a row',
+ '',
+ ].join('\n'),
+ );
+ const expected = u('root', [
+ heading('I โฅ unicode', 'i--unicode'),
+ heading('Dash-dash', 'dash-dash'),
+ heading('enโdash', 'endash'),
+ heading('emโdash', 'emdash'),
+ heading('๐ unicode emoji', '-unicode-emoji'),
+ heading('๐-๐ unicode emoji', '--unicode-emoji'),
+ heading('๐_๐ unicode emoji', '_-unicode-emoji'),
+ heading(null, ''),
+ heading(null, '-1'),
+ heading('Initial spaces', 'initial-spaces'),
+ heading('Final spaces', 'final-spaces'),
+ heading('Duplicate', 'duplicate'),
+ heading('Duplicate', 'duplicate-1'),
+ heading(':ok: No underscore', 'ok-no-underscore'),
+ heading(':ok_hand: Single', 'ok_hand-single'),
+ heading(
+ ':ok_hand::hatched_chick: Two in a row with no spaces',
+ 'ok_handhatched_chick-two-in-a-row-with-no-spaces',
+ ),
+ heading(
+ ':ok_hand: :hatched_chick: Two in a row',
+ 'ok_hand-hatched_chick-two-in-a-row',
+ ),
+ ]);
+
+ expect(result).toEqual(expected);
+ });
+
+ test('should generate slug from only text contents of headings if they contains HTML tags', () => {
+ const result = process('# \n');
+ const expected = u('root', [
+ u(
+ 'heading',
+ {
+ depth: 1,
+ data: {hProperties: {id: 'normal'}, id: 'normal'},
+ },
+ [
+ u('html', ''),
+ ],
+ ),
+ ]);
+
+ expect(result).toEqual(expected);
+ });
+});
diff --git a/packages/docusaurus-mdx-loader/src/remark/slug/index.js b/packages/docusaurus-mdx-loader/src/remark/slug/index.js
new file mode 100644
index 0000000000..61d72de6ef
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/slug/index.js
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/* Based on remark-slug (https://github.com/remarkjs/remark-slug) */
+
+const visit = require('unist-util-visit');
+const toString = require('mdast-util-to-string');
+const slugs = require('github-slugger')();
+
+function slug() {
+ const transformer = ast => {
+ slugs.reset();
+
+ function visitor(headingNode) {
+ const data = headingNode.data || (headingNode.data = {}); // eslint-disable-line
+ const properties = data.hProperties || (data.hProperties = {});
+ let {id} = properties;
+
+ if (id) {
+ id = slugs.slug(id, true);
+ } else {
+ const headingTextNodes =
+ headingNode.children.find(
+ ({type}) => !['html', 'jsx'].includes(type),
+ ) || headingNode;
+ id = slugs.slug(toString(headingTextNodes));
+ }
+
+ data.id = id;
+ properties.id = id;
+ }
+
+ visit(ast, 'heading', visitor);
+ };
+
+ return transformer;
+}
+
+module.exports = slug;