mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 15:47:23 +02:00
feat(v2): add ability to set custom heading id (#4222)
* feat(v2): add ability to set custom heading id * Add cli command * Fix slugger * write-heading-ids doc + add in commands/templates * refactor + add tests for writeHeadingIds * polish writeHeadingIds * polish writeHeadingIds * remove i18n goals todo section as the remaining items are quite abstract/useless * fix edge case with 2 md links in heading * extract parseMarkdownHeadingId helper function * refactor using the shared parseMarkdownHeadingId utility fn * change logic of edge case * Handle edge case * Document explicit ids feature Co-authored-by: slorber <lorber.sebastien@gmail.com>
This commit is contained in:
parent
6be0bd41b0
commit
96e7fcef25
26 changed files with 594 additions and 71 deletions
|
@ -11,7 +11,7 @@ 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 unwrapMdxCodeBlocks = require('./remark/unwrapMdxCodeBlocks');
|
||||
const transformImage = require('./remark/transformImage');
|
||||
|
@ -19,7 +19,7 @@ const transformLinks = require('./remark/transformLinks');
|
|||
|
||||
const DEFAULT_OPTIONS = {
|
||||
rehypePlugins: [],
|
||||
remarkPlugins: [unwrapMdxCodeBlocks, emoji, slug, toc],
|
||||
remarkPlugins: [unwrapMdxCodeBlocks, emoji, headings, toc],
|
||||
};
|
||||
|
||||
module.exports = async function docusaurusMdxLoader(fileString) {
|
||||
|
|
|
@ -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('# <span class="normal-header">Normal</span>\n');
|
||||
const expected = u('root', [
|
||||
u(
|
||||
|
@ -244,4 +246,70 @@ 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}
|
||||
|
||||
# With *Bold* hello{#custom-withbold-hello}
|
||||
|
||||
# With *Bold* hello2 {#custom-withbold-hello2}
|
||||
|
||||
# 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: 'custom-withbold-hello',
|
||||
text: 'With Bold hello',
|
||||
},
|
||||
{
|
||||
id: 'custom-withbold-hello2',
|
||||
text: 'With Bold hello2',
|
||||
},
|
||||
{
|
||||
id: 'this_is_custom_id',
|
||||
text: 'Snake-cased ID',
|
||||
},
|
||||
{
|
||||
id: 'no-custom-id',
|
||||
text: 'No custom ID',
|
||||
},
|
||||
{
|
||||
id: 'id-only',
|
||||
text: '',
|
||||
},
|
||||
{
|
||||
id: 'text-after-custom-id',
|
||||
text: '{#text-after} custom ID',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
74
packages/docusaurus-mdx-loader/src/remark/headings/index.js
Normal file
74
packages/docusaurus-mdx-loader/src/remark/headings/index.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* 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) and gatsby-remark-autolink-headers (https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-remark-autolink-headers) */
|
||||
|
||||
const {parseMarkdownHeadingId} = require('@docusaurus/utils');
|
||||
const visit = require('unist-util-visit');
|
||||
const toString = require('mdast-util-to-string');
|
||||
const slugs = require('github-slugger')();
|
||||
|
||||
function headings() {
|
||||
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.filter(
|
||||
({type}) => !['html', 'jsx'].includes(type),
|
||||
);
|
||||
const heading = toString(
|
||||
headingTextNodes.length > 0
|
||||
? {children: headingTextNodes}
|
||||
: headingNode,
|
||||
);
|
||||
|
||||
// Support explicit heading IDs
|
||||
const parsedHeading = parseMarkdownHeadingId(heading);
|
||||
|
||||
id = parsedHeading.id || slugs.slug(heading);
|
||||
|
||||
if (parsedHeading.id) {
|
||||
// When there's an id, it is always in the last child node
|
||||
// Sometimes heading is in multiple "parts" (** syntax creates a child node):
|
||||
// ## part1 *part2* part3 {#id}
|
||||
const lastNode =
|
||||
headingNode.children[headingNode.children.length - 1];
|
||||
|
||||
if (headingNode.children.length > 1) {
|
||||
const lastNodeText = parseMarkdownHeadingId(lastNode.value).text;
|
||||
// When last part contains test+id, remove the id
|
||||
if (lastNodeText) {
|
||||
lastNode.value = lastNodeText;
|
||||
}
|
||||
// When last part contains only the id: completely remove that node
|
||||
else {
|
||||
headingNode.children.pop();
|
||||
}
|
||||
} else {
|
||||
lastNode.value = parsedHeading.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data.id = id;
|
||||
properties.id = id;
|
||||
}
|
||||
|
||||
visit(ast, 'heading', visitor);
|
||||
};
|
||||
|
||||
return transformer;
|
||||
}
|
||||
|
||||
module.exports = headings;
|
|
@ -1,46 +0,0 @@
|
|||
/**
|
||||
* 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.filter(
|
||||
({type}) => !['html', 'jsx'].includes(type),
|
||||
);
|
||||
const normalizedHeadingNode =
|
||||
headingTextNodes.length > 0
|
||||
? {children: headingTextNodes}
|
||||
: headingNode;
|
||||
id = slugs.slug(toString(normalizedHeadingNode));
|
||||
}
|
||||
|
||||
data.id = id;
|
||||
properties.id = id;
|
||||
}
|
||||
|
||||
visit(ast, 'heading', visitor);
|
||||
};
|
||||
|
||||
return transformer;
|
||||
}
|
||||
|
||||
module.exports = slug;
|
|
@ -10,13 +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';
|
||||
import headings from '../../headings/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(headings)
|
||||
.use(mdx)
|
||||
.use(plugin, options)
|
||||
.process(file);
|
||||
|
|
|
@ -10,13 +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';
|
||||
import headings from '../../headings/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(headings)
|
||||
.use(mdx)
|
||||
.use(plugin, {...options, filePath: path})
|
||||
.process(file);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue