mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-14 09:37:37 +02:00
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:
parent
b11c24b752
commit
41ef333e47
28 changed files with 206 additions and 36 deletions
|
@ -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) {
|
||||
|
|
|
@ -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><Head /></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',
|
|
@ -1,4 +1,4 @@
|
|||
export const rightToc = ['replaceMe'];
|
||||
export const toc = ['replaceMe'];
|
||||
|
||||
## Thanos
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
|
@ -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 */
|
||||
|
||||
/**
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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}\""
|
||||
|
|
|
@ -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');
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
15
packages/docusaurus-theme-classic/src/types.d.ts
vendored
15
packages/docusaurus-theme-classic/src/types.d.ts
vendored
|
@ -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';
|
||||
|
|
4
packages/docusaurus-types/src/index.d.ts
vendored
4
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue