feat(v2): inline table-of-contents + refactor TOC (#3904)

* Add TOCInline theme component

* Add TOCInline theme component doc + migration guide

* remove useless getPathsToWatch on classic theme

* rename rightToc to toc

* add temp theme-bootstrap TOCInline comp to fix build issue
This commit is contained in:
Sébastien Lorber 2020-12-11 16:30:53 +01:00 committed by GitHub
parent b11c24b752
commit 41ef333e47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 206 additions and 36 deletions

View file

@ -12,13 +12,13 @@ const emoji = require('remark-emoji');
const matter = require('gray-matter');
const stringifyObject = require('stringify-object');
const slug = require('./remark/slug');
const rightToc = require('./remark/rightToc');
const toc = require('./remark/toc');
const transformImage = require('./remark/transformImage');
const transformLinks = require('./remark/transformLinks');
const DEFAULT_OPTIONS = {
rehypePlugins: [],
remarkPlugins: [emoji, slug, rightToc],
remarkPlugins: [emoji, slug, toc],
};
module.exports = async function docusaurusMdxLoader(fileString) {

View file

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`inline code should be escaped 1`] = `
"export const rightToc = [
"export const toc = [
{
value: '<code>&lt;Head /&gt;</code>',
id: 'head-',
@ -43,7 +43,7 @@ exports[`inline code should be escaped 1`] = `
`;
exports[`non text phrasing content 1`] = `
"export const rightToc = [
"export const toc = [
{
value: '<em>Emphasis</em>',
id: 'emphasis',

View file

@ -1,4 +1,4 @@
export const rightToc = ['replaceMe'];
export const toc = ['replaceMe'];
## Thanos

View file

@ -37,7 +37,7 @@ test('inline code should be escaped', async () => {
test('text content', async () => {
const result = await processFixture('just-content');
expect(result).toMatchInlineSnapshot(`
"export const rightToc = [
"export const toc = [
{
value: 'Endi',
id: 'endi',
@ -83,7 +83,7 @@ test('text content', async () => {
test('should export even with existing name', async () => {
const result = await processFixture('name-exist');
expect(result).toMatchInlineSnapshot(`
"export const rightToc = [
"export const toc = [
{
value: 'Thanos',
id: 'thanos',
@ -167,7 +167,7 @@ test('should insert below imports', async () => {
import somethingElse from 'something-else';
export const rightToc = [
export const toc = [
{
value: 'Title',
id: 'title',
@ -200,7 +200,7 @@ test('should insert below imports', async () => {
test('empty headings', async () => {
const result = await processFixture('empty-headings');
expect(result).toMatchInlineSnapshot(`
"export const rightToc = [];
"export const toc = [];
# Ignore this

View file

@ -59,7 +59,7 @@ const getOrCreateExistingTargetIndex = (children, name) => {
};
const plugin = (options = {}) => {
const name = options.name || 'rightToc';
const name = options.name || 'toc';
const transformer = (node) => {
const headings = search(node);

View file

@ -11,7 +11,7 @@ const toString = require('mdast-util-to-string');
const visit = require('unist-util-visit');
const {toValue} = require('../utils');
/** @typedef {import('@docusaurus/types').MarkdownRightTableOfContents} TOC */
/** @typedef {import('@docusaurus/types').TOCItem} TOC */
/** @typedef {import('unist').Node} Node */
/**

View file

@ -24,7 +24,7 @@ declare module '@theme/BlogSidebar' {
}
declare module '@theme/BlogPostPage' {
import type {MarkdownRightTableOfContents} from '@docusaurus/types';
import type {TOCItem} from '@docusaurus/types';
import type {BlogSidebar} from '@theme/BlogSidebar';
export type FrontMatter = {
@ -61,7 +61,7 @@ declare module '@theme/BlogPostPage' {
export type Content = {
readonly frontMatter: FrontMatter;
readonly metadata: Metadata;
readonly rightToc: readonly MarkdownRightTableOfContents[];
readonly toc: readonly TOCItem[];
(): JSX.Element;
};

View file

@ -46,7 +46,7 @@ declare module '@docusaurus/plugin-content-docs-types' {
}
declare module '@theme/DocItem' {
import type {MarkdownRightTableOfContents} from '@docusaurus/types';
import type {TOCItem} from '@docusaurus/types';
export type DocumentRoute = {
readonly component: () => JSX.Element;
@ -80,7 +80,7 @@ declare module '@theme/DocItem' {
readonly content: {
readonly frontMatter: FrontMatter;
readonly metadata: Metadata;
readonly rightToc: readonly MarkdownRightTableOfContents[];
readonly toc: readonly TOCItem[];
(): JSX.Element;
};
};

View file

@ -6,7 +6,7 @@
*/
declare module '@theme/MDXPage' {
import type {MarkdownRightTableOfContents} from '@docusaurus/types';
import type {TOCItem} from '@docusaurus/types';
export type Props = {
readonly content: {
@ -17,7 +17,7 @@ declare module '@theme/MDXPage' {
readonly hide_table_of_contents?: string;
};
readonly metadata: {readonly permalink: string};
readonly rightToc: readonly MarkdownRightTableOfContents[];
readonly toc: readonly TOCItem[];
(): JSX.Element;
};
};

View file

@ -0,0 +1,14 @@
/**
* 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.
*/
import React from 'react';
function TOCInline(_props: any): JSX.Element {
return <div>TODO bootstrap toc</div>;
}
export default TOCInline;

View file

@ -15,7 +15,7 @@
"license": "MIT",
"scripts": {
"build": "tsc --noEmit && yarn babel:lib && yarn babel:lib-next && yarn prettier",
"watch": "yarn babel:lib --watch",
"watch": "concurrently -n \"lib,lib-next\" --kill-others \"yarn babel:lib --watch\" \"yarn babel:lib-next --watch\"",
"babel:lib": "cross-env BABEL_ENV=lib babel src -d lib --extensions \".tsx,.ts\" --ignore \"**/*.d.ts\" --copy-files",
"babel:lib-next": "cross-env BABEL_ENV=lib-next babel src -d lib-next --extensions \".tsx,.ts\" --ignore \"**/*.d.ts\" --copy-files",
"prettier": "prettier --config ../../.prettierrc --ignore-path ../../.prettierignore --write \"**/*.{js,ts}\""

View file

@ -72,6 +72,16 @@ export default function docusaurusThemeClassic(
return {
name: 'docusaurus-theme-classic',
/*
Does not seem needed: webpack can already hot reload theme files
getPathsToWatch() {
return [
path.join(__dirname, '..', 'lib'),
path.join(__dirname, '..', 'lib-next'),
];
},
*/
getThemePath() {
return path.join(__dirname, '..', 'lib-next', 'theme');
},

View file

@ -53,9 +53,9 @@ function BlogPostPage(props: Props): JSX.Element {
</div>
)}
</main>
{!hideTableOfContents && BlogPostContents.rightToc && (
{!hideTableOfContents && BlogPostContents.toc && (
<div className="col col--2">
<TOC headings={BlogPostContents.rightToc} />
<TOC toc={BlogPostContents.toc} />
</div>
)}
</div>

View file

@ -162,9 +162,9 @@ function DocItem(props: Props): JSX.Element {
</div>
</div>
</div>
{!hideTableOfContents && DocContent.rightToc && (
{!hideTableOfContents && DocContent.toc && (
<div className="col col--3">
<TOC headings={DocContent.rightToc} />
<TOC toc={DocContent.toc} />
</div>
)}
</div>

View file

@ -41,9 +41,9 @@ function MDXPage(props: Props): JSX.Element {
</MDXProvider>
</div>
</div>
{!hideTableOfContents && MDXPageContent.rightToc && (
{!hideTableOfContents && MDXPageContent.toc && (
<div className="col col--2">
<TOC headings={MDXPageContent.rightToc} />
<TOC toc={MDXPageContent.toc} />
</div>
)}
</div>

View file

@ -10,14 +10,21 @@ import clsx from 'clsx';
import useTOCHighlight from '@theme/hooks/useTOCHighlight';
import type {TOCProps} from '@theme/TOC';
import styles from './styles.module.css';
import {TOCItem} from '@docusaurus/types';
const LINK_CLASS_NAME = 'table-of-contents__link';
const ACTIVE_LINK_CLASS_NAME = 'table-of-contents__link--active';
const TOP_OFFSET = 100;
/* eslint-disable jsx-a11y/control-has-associated-label */
function Headings({headings, isChild}: TOCProps & {isChild?: boolean}) {
if (!headings.length) {
function Headings({
toc,
isChild,
}: {
toc: readonly TOCItem[];
isChild?: boolean;
}) {
if (!toc.length) {
return null;
}
return (
@ -25,7 +32,7 @@ function Headings({headings, isChild}: TOCProps & {isChild?: boolean}) {
className={
isChild ? '' : 'table-of-contents table-of-contents__left-border'
}>
{headings.map((heading) => (
{toc.map((heading) => (
<li key={heading.id}>
<a
href={`#${heading.id}`}
@ -34,18 +41,18 @@ function Headings({headings, isChild}: TOCProps & {isChild?: boolean}) {
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{__html: heading.value}}
/>
<Headings isChild headings={heading.children} />
<Headings isChild toc={heading.children} />
</li>
))}
</ul>
);
}
function TOC({headings}: TOCProps): JSX.Element {
function TOC({toc}: TOCProps): JSX.Element {
useTOCHighlight(LINK_CLASS_NAME, ACTIVE_LINK_CLASS_NAME, TOP_OFFSET);
return (
<div className={clsx(styles.tableOfContents, 'thin-scrollbar')}>
<Headings headings={headings} />
<Headings toc={toc} />
</div>
);
}

View file

@ -0,0 +1,53 @@
/**
* 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.
*/
import React from 'react';
import clsx from 'clsx';
import type {TOCProps} from '@theme/TOC';
import styles from './styles.module.css';
import {TOCItem} from '@docusaurus/types';
const LINK_CLASS_NAME = styles['table-of-contents__link--inline'];
/* eslint-disable jsx-a11y/control-has-associated-label */
function HeadingsInline({
toc,
isChild,
}: {
toc: readonly TOCItem[];
isChild?: boolean;
}) {
if (!toc.length) {
return null;
}
return (
<ul className={isChild ? '' : 'table-of-contents'}>
{toc.map((heading) => (
<li key={heading.id}>
<a
href={`#${heading.id}`}
className={LINK_CLASS_NAME}
// Developer provided the HTML, so assume it's safe.
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{__html: heading.value}}
/>
<HeadingsInline isChild toc={heading.children} />
</li>
))}
</ul>
);
}
function TOCInline({toc}: TOCProps): JSX.Element {
return (
<div className={clsx(styles.tableOfContentsInline)}>
<HeadingsInline toc={toc} />
</div>
);
}
export default TOCInline;

View file

@ -0,0 +1,14 @@
/**
* 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.
*/
.tableOfContentsInline ul {
list-style-type: disc;
}
.table-of-contents__link--inline {
color: var(--ifm-toc-link-color);
}

View file

@ -413,16 +413,27 @@ declare module '@theme/ThemeProvider' {
}
declare module '@theme/TOC' {
import type {MarkdownRightTableOfContents} from '@docusaurus/types';
import type {TOCItem} from '@docusaurus/types';
export type TOCProps = {
readonly headings: readonly MarkdownRightTableOfContents[];
readonly toc: readonly TOCItem[];
};
const TOC: (props: TOCProps) => JSX.Element;
export default TOC;
}
declare module '@theme/TOCInline' {
import type {TOCItem} from '@docusaurus/types';
export type TOCInlineProps = {
readonly toc: readonly TOCItem[];
};
const TOCInline: (props: TOCInlineProps) => JSX.Element;
export default TOCInline;
}
declare module '@theme/Toggle' {
import {ComponentProps} from 'react';
import ReactToggle from 'react-toggle';

View file

@ -344,8 +344,8 @@ export interface ValidationSchema<T> {
append(data: any): ValidationSchema<T>;
}
export interface MarkdownRightTableOfContents {
export interface TOCItem {
readonly value: string;
readonly id: string;
readonly children: MarkdownRightTableOfContents[];
readonly children: TOCItem[];
}

View file

@ -20,6 +20,12 @@ Once your website is bootstrapped, the website source will contain the Docusauru
}
```
## Index
import TOCInline from "@theme/TOCInline"
<TOCInline toc={toc[1].children}/>
## Docusaurus CLI commands
Below is a list of Docusaurus CLI commands and their usages:

View file

@ -471,6 +471,61 @@ import TabItem from '@theme/TabItem';
<TabItem value="banana">This is a banana 🍌</TabItem>
</Tabs>
## Inline table of contents
Each markdown document displays a tab of content on the top-right corner.
But it is also possible to display an inline table of contents directly inside a markdown document, thanks to MDX.
### Full table of contents
The `toc` variable is available in any MDX document, and contain all the top level headings of a MDX document.
```jsx
import TOCInline from '@theme/TOCInline';
<TOCInline toc={toc} />;
```
import TOCInline from '@theme/TOCInline';
<BrowserWindow>
<TOCInline toc={toc} />
</BrowserWindow>
### Custom table of contents
The `toc` props is just a list of table of contents items:
```ts
type TOCItem = {
value: string;
id: string;
children: TOCItem[];
};
```
You can create this TOC tree manually, or derive a new TOC tree from the `toc` variable:
```jsx
import TOCInline from '@theme/TOCInline';
<TOCInline
toc={
// Only show 4th and 5th top-level heading
[toc[3], toc[4]]
}
/>;
```
<BrowserWindow>
<TOCInline toc={[toc[3], toc[4]]} />
</BrowserWindow>
## Callouts/admonitions
In addition to the basic Markdown syntax, we use [remark-admonitions](https://github.com/elviswolcott/remark-admonitions) alongside MDX to add support for admonitions. Admonitions are wrapped by a set of 3 colons.

View file

@ -554,9 +554,9 @@ The following code could be helpful for migration of various pages:
## Content
### Remove AUTOGENERATED_TABLE_OF_CONTENTS
### Replace AUTOGENERATED_TABLE_OF_CONTENTS
This feature is deprecated. You may read more about it in [this issue](https://github.com/facebook/docusaurus/issues/1549). If you need the feature, use [remark-toc](https://github.com/remarkjs/remark-toc) instead and pass it to docs plugin's `remarkPlugins` option.
This feature is replaced by [inline table of content](../markdown-features.mdx#inline-table-of-contents)
### Update Markdown syntax to be MDX-compatible