mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-10 23:57:22 +02:00
feat(v2): allow specifying TOC max depth (themeConfig + frontMatter) (#5578)
* feat: add all TOC levels to MDX loader * feat: add theme-level config for heading depth * test: add remark MDX loader test * fix: limit maxDepth validation to H2 - H6 * refactor: set default `maxDepth` using `joi` * refactor: `maxDepth` -> `maxHeadingLevel * refactor: invert underlying TOC depth API * refactor: make TOC algorithm level-aware * feat: add support for per-doc TOC heading levels * feat: support document-level heading levels for blog * fix: correct validation for toc level frontmatter * fix: ensure TOC doesn't generate redundant DOM * perf: simpler TOC heading search alg * docs: document heading level props for `TOCInline` * Update website/docs/guides/markdown-features/markdown-features-inline-toc.mdx Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> * docs: fix docs (again) * create dedicated test file for heading searching logic: exhaustive tests will be simpler to write * toc search: add real-world test * fix test * add dogfooding tests for toc min/max * add test for min/max toc frontmatter * reverse min/max order * add theme minHeadingLevel + tests * simpler TOC rendering logic * simplify TOC implementation (temp, WIP) * reverse unnatural order for minHeadingLevel/maxHeadingLevel * add TOC dogfooding tests to all content plugins * expose toc min/max heading level frontmatter to all 3 content plugins * refactor blogLayout: accept toc ReactElement directly * move toc utils to theme-common * add tests for filterTOC * create new generic TOCItems component * useless css file copied * fix toc highlighting className conflicts * update doc * fix types Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> Co-authored-by: slorber <lorber.sebastien@gmail.com>
This commit is contained in:
parent
caba1e4908
commit
c86dfbda61
50 changed files with 1522 additions and 214 deletions
|
@ -9,24 +9,29 @@ exports[`inline code should be escaped 1`] = `
|
||||||
{
|
{
|
||||||
value: '<code><Head>Test</Head></code>',
|
value: '<code><Head>Test</Head></code>',
|
||||||
id: 'headtesthead',
|
id: 'headtesthead',
|
||||||
children: []
|
children: [],
|
||||||
|
level: 3
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
level: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '<code><div /></code>',
|
value: '<code><div /></code>',
|
||||||
id: 'div-',
|
id: 'div-',
|
||||||
children: []
|
children: [],
|
||||||
|
level: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '<code><div> Test </div></code>',
|
value: '<code><div> Test </div></code>',
|
||||||
id: 'div-test-div',
|
id: 'div-test-div',
|
||||||
children: []
|
children: [],
|
||||||
|
level: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '<code><div><i>Test</i></div></code>',
|
value: '<code><div><i>Test</i></div></code>',
|
||||||
id: 'divitestidiv',
|
id: 'divitestidiv',
|
||||||
children: []
|
children: [],
|
||||||
|
level: 2
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -51,24 +56,29 @@ exports[`non text phrasing content 1`] = `
|
||||||
{
|
{
|
||||||
value: '<strong>Importance</strong>',
|
value: '<strong>Importance</strong>',
|
||||||
id: 'importance',
|
id: 'importance',
|
||||||
children: []
|
children: [],
|
||||||
|
level: 3
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
level: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '<del>Strikethrough</del>',
|
value: '<del>Strikethrough</del>',
|
||||||
id: 'strikethrough',
|
id: 'strikethrough',
|
||||||
children: []
|
children: [],
|
||||||
|
level: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '<i>HTML</i>',
|
value: '<i>HTML</i>',
|
||||||
id: 'html',
|
id: 'html',
|
||||||
children: []
|
children: [],
|
||||||
|
level: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '<code>inline.code()</code>',
|
value: '<code>inline.code()</code>',
|
||||||
id: 'inlinecode',
|
id: 'inlinecode',
|
||||||
children: []
|
children: [],
|
||||||
|
level: 2
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ import vfile from 'to-vfile';
|
||||||
import plugin from '../index';
|
import plugin from '../index';
|
||||||
import headings from '../../headings/index';
|
import headings from '../../headings/index';
|
||||||
|
|
||||||
const processFixture = async (name, options) => {
|
const processFixture = async (name, options?) => {
|
||||||
const path = join(__dirname, 'fixtures', `${name}.md`);
|
const path = join(__dirname, 'fixtures', `${name}.md`);
|
||||||
const file = await vfile.read(path);
|
const file = await vfile.read(path);
|
||||||
const result = await remark()
|
const result = await remark()
|
||||||
|
@ -41,7 +41,8 @@ test('text content', async () => {
|
||||||
{
|
{
|
||||||
value: 'Endi',
|
value: 'Endi',
|
||||||
id: 'endi',
|
id: 'endi',
|
||||||
children: []
|
children: [],
|
||||||
|
level: 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'Endi',
|
value: 'Endi',
|
||||||
|
@ -50,14 +51,17 @@ test('text content', async () => {
|
||||||
{
|
{
|
||||||
value: 'Yangshun',
|
value: 'Yangshun',
|
||||||
id: 'yangshun',
|
id: 'yangshun',
|
||||||
children: []
|
children: [],
|
||||||
|
level: 3
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
level: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'I ♥ unicode.',
|
value: 'I ♥ unicode.',
|
||||||
id: 'i--unicode',
|
id: 'i--unicode',
|
||||||
children: []
|
children: [],
|
||||||
|
level: 2
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -87,7 +91,8 @@ test('should export even with existing name', async () => {
|
||||||
{
|
{
|
||||||
value: 'Thanos',
|
value: 'Thanos',
|
||||||
id: 'thanos',
|
id: 'thanos',
|
||||||
children: []
|
children: [],
|
||||||
|
level: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'Tony Stark',
|
value: 'Tony Stark',
|
||||||
|
@ -96,9 +101,11 @@ test('should export even with existing name', async () => {
|
||||||
{
|
{
|
||||||
value: 'Avengers',
|
value: 'Avengers',
|
||||||
id: 'avengers',
|
id: 'avengers',
|
||||||
children: []
|
children: [],
|
||||||
|
level: 3
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
level: 2
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -121,7 +128,8 @@ test('should export with custom name', async () => {
|
||||||
{
|
{
|
||||||
value: 'Endi',
|
value: 'Endi',
|
||||||
id: 'endi',
|
id: 'endi',
|
||||||
children: []
|
children: [],
|
||||||
|
level: 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'Endi',
|
value: 'Endi',
|
||||||
|
@ -130,14 +138,17 @@ test('should export with custom name', async () => {
|
||||||
{
|
{
|
||||||
value: 'Yangshun',
|
value: 'Yangshun',
|
||||||
id: 'yangshun',
|
id: 'yangshun',
|
||||||
children: []
|
children: [],
|
||||||
|
level: 3
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
level: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'I ♥ unicode.',
|
value: 'I ♥ unicode.',
|
||||||
id: 'i--unicode',
|
id: 'i--unicode',
|
||||||
children: []
|
children: [],
|
||||||
|
level: 2
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -171,7 +182,8 @@ test('should insert below imports', async () => {
|
||||||
{
|
{
|
||||||
value: 'Title',
|
value: 'Title',
|
||||||
id: 'title',
|
id: 'title',
|
||||||
children: []
|
children: [],
|
||||||
|
level: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'Test',
|
value: 'Test',
|
||||||
|
@ -180,9 +192,11 @@ test('should insert below imports', async () => {
|
||||||
{
|
{
|
||||||
value: 'Again',
|
value: 'Again',
|
||||||
id: 'again',
|
id: 'again',
|
||||||
children: []
|
children: [],
|
||||||
|
level: 3
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
level: 2
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,182 @@
|
||||||
|
/**
|
||||||
|
* 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 remark from 'remark';
|
||||||
|
import mdx from 'remark-mdx';
|
||||||
|
import search from '../search';
|
||||||
|
import headings from '../../headings/index';
|
||||||
|
|
||||||
|
const getHeadings = async (mdText: string) => {
|
||||||
|
const node = remark().parse(mdText);
|
||||||
|
const result = await remark().use(headings).use(mdx).run(node);
|
||||||
|
return search(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
test('should process all heading levels', async () => {
|
||||||
|
const md = `
|
||||||
|
# Alpha
|
||||||
|
|
||||||
|
## Bravo
|
||||||
|
|
||||||
|
### Charlie
|
||||||
|
|
||||||
|
#### Delta
|
||||||
|
|
||||||
|
##### Echo
|
||||||
|
|
||||||
|
###### Foxtrot
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
expect(await getHeadings(md)).toEqual([
|
||||||
|
{
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
children: [],
|
||||||
|
id: 'foxtrot',
|
||||||
|
level: 6,
|
||||||
|
value: 'Foxtrot',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 'echo',
|
||||||
|
level: 5,
|
||||||
|
value: 'Echo',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 'delta',
|
||||||
|
level: 4,
|
||||||
|
value: 'Delta',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 'charlie',
|
||||||
|
level: 3,
|
||||||
|
value: 'Charlie',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 'bravo',
|
||||||
|
level: 2,
|
||||||
|
value: 'Bravo',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should process real-world well-formatted md', async () => {
|
||||||
|
const md = `
|
||||||
|
# title
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
## section 1
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
### subsection 1-1
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
#### subsection 1-1-1
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
#### subsection 1-1-2
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
### subsection 1-2
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
### subsection 1-3
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
## section 2
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
### subsection 2-1
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
### subsection 2-1
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
## section 3
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
### subsection 3-1
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
### subsection 3-2
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
expect(await getHeadings(md)).toEqual([
|
||||||
|
{
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
children: [],
|
||||||
|
id: 'subsection-1-1-1',
|
||||||
|
level: 4,
|
||||||
|
value: 'subsection 1-1-1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: [],
|
||||||
|
id: 'subsection-1-1-2',
|
||||||
|
level: 4,
|
||||||
|
value: 'subsection 1-1-2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 'subsection-1-1',
|
||||||
|
level: 3,
|
||||||
|
value: 'subsection 1-1',
|
||||||
|
},
|
||||||
|
{children: [], id: 'subsection-1-2', level: 3, value: 'subsection 1-2'},
|
||||||
|
{children: [], id: 'subsection-1-3', level: 3, value: 'subsection 1-3'},
|
||||||
|
],
|
||||||
|
id: 'section-1',
|
||||||
|
level: 2,
|
||||||
|
value: 'section 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: [
|
||||||
|
{children: [], id: 'subsection-2-1', level: 3, value: 'subsection 2-1'},
|
||||||
|
{
|
||||||
|
children: [],
|
||||||
|
id: 'subsection-2-1-1',
|
||||||
|
level: 3,
|
||||||
|
value: 'subsection 2-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
id: 'section-2',
|
||||||
|
level: 2,
|
||||||
|
value: 'section 2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: [
|
||||||
|
{children: [], id: 'subsection-3-1', level: 3, value: 'subsection 3-1'},
|
||||||
|
{children: [], id: 'subsection-3-2', level: 3, value: 'subsection 3-2'},
|
||||||
|
],
|
||||||
|
id: 'section-3',
|
||||||
|
level: 2,
|
||||||
|
value: 'section 3',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
|
@ -8,40 +8,75 @@
|
||||||
import toString from 'mdast-util-to-string';
|
import toString from 'mdast-util-to-string';
|
||||||
import visit, {Visitor} from 'unist-util-visit';
|
import visit, {Visitor} from 'unist-util-visit';
|
||||||
import {toValue} from '../utils';
|
import {toValue} from '../utils';
|
||||||
import type {TOCItem as TOC} from '@docusaurus/types';
|
import type {TOCItem} from '@docusaurus/types';
|
||||||
import type {Node} from 'unist';
|
import type {Node} from 'unist';
|
||||||
import type {Heading} from 'mdast';
|
import type {Heading} from 'mdast';
|
||||||
|
|
||||||
// Visit all headings. We `slug` all headings (to account for
|
// Intermediate interface for TOC algorithm
|
||||||
// duplicates), but only take h2 and h3 headings.
|
interface SearchItem {
|
||||||
export default function search(node: Node): TOC[] {
|
node: TOCItem;
|
||||||
const headings: TOC[] = [];
|
level: number;
|
||||||
let current = -1;
|
parentIndex: number;
|
||||||
let currentDepth = 0;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Generate a TOC AST from the raw Markdown contents
|
||||||
|
*/
|
||||||
|
export default function search(node: Node): TOCItem[] {
|
||||||
|
const headings: SearchItem[] = [];
|
||||||
|
|
||||||
const visitor: Visitor<Heading> = (child, _index, parent) => {
|
const visitor: Visitor<Heading> = (child, _index, parent) => {
|
||||||
const value = toString(child);
|
const value = toString(child);
|
||||||
|
|
||||||
if (parent !== node || !value || child.depth > 3 || child.depth < 2) {
|
// depth:1 headings are titles and not included in the TOC
|
||||||
|
if (parent !== node || !value || child.depth < 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry: TOC = {
|
headings.push({
|
||||||
|
node: {
|
||||||
value: toValue(child),
|
value: toValue(child),
|
||||||
id: child.data!.id as string,
|
id: child.data!.id as string,
|
||||||
children: [],
|
children: [],
|
||||||
};
|
level: child.depth,
|
||||||
|
},
|
||||||
if (!headings.length || currentDepth >= child.depth) {
|
level: child.depth,
|
||||||
headings.push(entry);
|
parentIndex: -1,
|
||||||
current += 1;
|
});
|
||||||
currentDepth = child.depth;
|
|
||||||
} else {
|
|
||||||
headings[current].children.push(entry);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
visit(node, 'heading', visitor);
|
visit(node, 'heading', visitor);
|
||||||
|
|
||||||
return headings;
|
// Keep track of which previous index would be the current heading's direcy parent.
|
||||||
|
// Each entry <i> is the last index of the `headings` array at heading level <i>.
|
||||||
|
// We will modify these indices as we iterate through all headings.
|
||||||
|
// e.g. if an ### H3 was last seen at index 2, then prevIndexForLevel[3] === 2
|
||||||
|
// indices 0 and 1 will remain unused.
|
||||||
|
const prevIndexForLevel = Array(7).fill(-1);
|
||||||
|
|
||||||
|
headings.forEach((curr, currIndex) => {
|
||||||
|
// take the last seen index for each ancestor level. the highest
|
||||||
|
// index will be the direct ancestor of the current heading.
|
||||||
|
const ancestorLevelIndexes = prevIndexForLevel.slice(2, curr.level);
|
||||||
|
curr.parentIndex = Math.max(...ancestorLevelIndexes);
|
||||||
|
// mark that curr.level was last seen at the current index
|
||||||
|
prevIndexForLevel[curr.level] = currIndex;
|
||||||
|
});
|
||||||
|
|
||||||
|
const rootNodeIndexes: number[] = [];
|
||||||
|
|
||||||
|
// For a given parentIndex, add each Node into that parent's `children` array
|
||||||
|
headings.forEach((heading, i) => {
|
||||||
|
if (heading.parentIndex >= 0) {
|
||||||
|
headings[heading.parentIndex].node.children.push(heading.node);
|
||||||
|
} else {
|
||||||
|
rootNodeIndexes.push(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const toc = headings
|
||||||
|
.filter((_, k) => rootNodeIndexes.includes(k)) // only return root nodes
|
||||||
|
.map((heading) => heading.node); // only return Node, no metadata
|
||||||
|
return toc;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
URISchema,
|
URISchema,
|
||||||
validateFrontMatter,
|
validateFrontMatter,
|
||||||
FrontMatterTagsSchema,
|
FrontMatterTagsSchema,
|
||||||
|
FrontMatterTOCHeadingLevels,
|
||||||
} from '@docusaurus/utils-validation';
|
} from '@docusaurus/utils-validation';
|
||||||
import type {FrontMatterTag} from '@docusaurus/utils';
|
import type {FrontMatterTag} from '@docusaurus/utils';
|
||||||
|
|
||||||
|
@ -65,6 +66,8 @@ export type BlogPostFrontMatter = {
|
||||||
image?: string;
|
image?: string;
|
||||||
keywords?: string[];
|
keywords?: string[];
|
||||||
hide_table_of_contents?: boolean;
|
hide_table_of_contents?: boolean;
|
||||||
|
toc_min_heading_level?: number;
|
||||||
|
toc_max_heading_level?: number;
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -111,6 +114,8 @@ const BlogFrontMatterSchema = Joi.object<BlogPostFrontMatter>({
|
||||||
image: URISchema,
|
image: URISchema,
|
||||||
keywords: Joi.array().items(Joi.string().required()),
|
keywords: Joi.array().items(Joi.string().required()),
|
||||||
hide_table_of_contents: Joi.boolean(),
|
hide_table_of_contents: Joi.boolean(),
|
||||||
|
|
||||||
|
...FrontMatterTOCHeadingLevels,
|
||||||
}).messages({
|
}).messages({
|
||||||
'deprecate.error':
|
'deprecate.error':
|
||||||
'{#label} blog frontMatter field is deprecated. Please use {#alternative} instead.',
|
'{#label} blog frontMatter field is deprecated. Please use {#alternative} instead.',
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {DocFrontMatter} from '../types';
|
||||||
import escapeStringRegexp from 'escape-string-regexp';
|
import escapeStringRegexp from 'escape-string-regexp';
|
||||||
|
|
||||||
function testField(params: {
|
function testField(params: {
|
||||||
fieldName: keyof DocFrontMatter;
|
prefix: string;
|
||||||
validFrontMatters: DocFrontMatter[];
|
validFrontMatters: DocFrontMatter[];
|
||||||
convertibleFrontMatter?: [
|
convertibleFrontMatter?: [
|
||||||
ConvertableFrontMatter: Record<string, unknown>,
|
ConvertableFrontMatter: Record<string, unknown>,
|
||||||
|
@ -21,14 +21,13 @@ function testField(params: {
|
||||||
ErrorMessage: string,
|
ErrorMessage: string,
|
||||||
][];
|
][];
|
||||||
}) {
|
}) {
|
||||||
describe(`"${params.fieldName}" field`, () => {
|
test(`[${params.prefix}] accept valid values`, () => {
|
||||||
test('accept valid values', () => {
|
|
||||||
params.validFrontMatters.forEach((frontMatter) => {
|
params.validFrontMatters.forEach((frontMatter) => {
|
||||||
expect(validateDocFrontMatter(frontMatter)).toEqual(frontMatter);
|
expect(validateDocFrontMatter(frontMatter)).toEqual(frontMatter);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('convert valid values', () => {
|
test(`[${params.prefix}] convert valid values`, () => {
|
||||||
params.convertibleFrontMatter?.forEach(
|
params.convertibleFrontMatter?.forEach(
|
||||||
([convertibleFrontMatter, convertedFrontMatter]) => {
|
([convertibleFrontMatter, convertedFrontMatter]) => {
|
||||||
expect(validateDocFrontMatter(convertibleFrontMatter)).toEqual(
|
expect(validateDocFrontMatter(convertibleFrontMatter)).toEqual(
|
||||||
|
@ -38,7 +37,7 @@ function testField(params: {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('throw error for values', () => {
|
test(`[${params.prefix}] throw error for values`, () => {
|
||||||
params.invalidFrontMatters?.forEach(([frontMatter, message]) => {
|
params.invalidFrontMatters?.forEach(([frontMatter, message]) => {
|
||||||
try {
|
try {
|
||||||
validateDocFrontMatter(frontMatter);
|
validateDocFrontMatter(frontMatter);
|
||||||
|
@ -56,7 +55,6 @@ function testField(params: {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('validateDocFrontMatter', () => {
|
describe('validateDocFrontMatter', () => {
|
||||||
|
@ -73,7 +71,7 @@ describe('validateDocFrontMatter', () => {
|
||||||
|
|
||||||
describe('validateDocFrontMatter id', () => {
|
describe('validateDocFrontMatter id', () => {
|
||||||
testField({
|
testField({
|
||||||
fieldName: 'id',
|
prefix: 'id',
|
||||||
validFrontMatters: [{id: '123'}, {id: 'unique_id'}],
|
validFrontMatters: [{id: '123'}, {id: 'unique_id'}],
|
||||||
invalidFrontMatters: [[{id: ''}, 'is not allowed to be empty']],
|
invalidFrontMatters: [[{id: ''}, 'is not allowed to be empty']],
|
||||||
});
|
});
|
||||||
|
@ -81,7 +79,7 @@ describe('validateDocFrontMatter id', () => {
|
||||||
|
|
||||||
describe('validateDocFrontMatter title', () => {
|
describe('validateDocFrontMatter title', () => {
|
||||||
testField({
|
testField({
|
||||||
fieldName: 'title',
|
prefix: 'title',
|
||||||
validFrontMatters: [
|
validFrontMatters: [
|
||||||
// See https://github.com/facebook/docusaurus/issues/4591#issuecomment-822372398
|
// See https://github.com/facebook/docusaurus/issues/4591#issuecomment-822372398
|
||||||
{title: ''},
|
{title: ''},
|
||||||
|
@ -92,7 +90,7 @@ describe('validateDocFrontMatter title', () => {
|
||||||
|
|
||||||
describe('validateDocFrontMatter hide_title', () => {
|
describe('validateDocFrontMatter hide_title', () => {
|
||||||
testField({
|
testField({
|
||||||
fieldName: 'hide_title',
|
prefix: 'hide_title',
|
||||||
validFrontMatters: [{hide_title: true}, {hide_title: false}],
|
validFrontMatters: [{hide_title: true}, {hide_title: false}],
|
||||||
convertibleFrontMatter: [
|
convertibleFrontMatter: [
|
||||||
[{hide_title: 'true'}, {hide_title: true}],
|
[{hide_title: 'true'}, {hide_title: true}],
|
||||||
|
@ -108,7 +106,7 @@ describe('validateDocFrontMatter hide_title', () => {
|
||||||
|
|
||||||
describe('validateDocFrontMatter hide_table_of_contents', () => {
|
describe('validateDocFrontMatter hide_table_of_contents', () => {
|
||||||
testField({
|
testField({
|
||||||
fieldName: 'hide_table_of_contents',
|
prefix: 'hide_table_of_contents',
|
||||||
validFrontMatters: [
|
validFrontMatters: [
|
||||||
{hide_table_of_contents: true},
|
{hide_table_of_contents: true},
|
||||||
{hide_table_of_contents: false},
|
{hide_table_of_contents: false},
|
||||||
|
@ -127,7 +125,7 @@ describe('validateDocFrontMatter hide_table_of_contents', () => {
|
||||||
|
|
||||||
describe('validateDocFrontMatter keywords', () => {
|
describe('validateDocFrontMatter keywords', () => {
|
||||||
testField({
|
testField({
|
||||||
fieldName: 'keywords',
|
prefix: 'keywords',
|
||||||
validFrontMatters: [
|
validFrontMatters: [
|
||||||
{keywords: ['hello']},
|
{keywords: ['hello']},
|
||||||
{keywords: ['hello', 'world']},
|
{keywords: ['hello', 'world']},
|
||||||
|
@ -144,7 +142,7 @@ describe('validateDocFrontMatter keywords', () => {
|
||||||
|
|
||||||
describe('validateDocFrontMatter image', () => {
|
describe('validateDocFrontMatter image', () => {
|
||||||
testField({
|
testField({
|
||||||
fieldName: 'image',
|
prefix: 'image',
|
||||||
validFrontMatters: [
|
validFrontMatters: [
|
||||||
{image: 'https://docusaurus.io/blog/image.png'},
|
{image: 'https://docusaurus.io/blog/image.png'},
|
||||||
{image: '/absolute/image.png'},
|
{image: '/absolute/image.png'},
|
||||||
|
@ -158,7 +156,7 @@ describe('validateDocFrontMatter image', () => {
|
||||||
|
|
||||||
describe('validateDocFrontMatter description', () => {
|
describe('validateDocFrontMatter description', () => {
|
||||||
testField({
|
testField({
|
||||||
fieldName: 'description',
|
prefix: 'description',
|
||||||
validFrontMatters: [
|
validFrontMatters: [
|
||||||
// See https://github.com/facebook/docusaurus/issues/4591#issuecomment-822372398
|
// See https://github.com/facebook/docusaurus/issues/4591#issuecomment-822372398
|
||||||
{description: ''},
|
{description: ''},
|
||||||
|
@ -169,7 +167,7 @@ describe('validateDocFrontMatter description', () => {
|
||||||
|
|
||||||
describe('validateDocFrontMatter slug', () => {
|
describe('validateDocFrontMatter slug', () => {
|
||||||
testField({
|
testField({
|
||||||
fieldName: 'slug',
|
prefix: 'slug',
|
||||||
validFrontMatters: [
|
validFrontMatters: [
|
||||||
{slug: '/'},
|
{slug: '/'},
|
||||||
{slug: 'slug'},
|
{slug: 'slug'},
|
||||||
|
@ -186,7 +184,7 @@ describe('validateDocFrontMatter slug', () => {
|
||||||
|
|
||||||
describe('validateDocFrontMatter sidebar_label', () => {
|
describe('validateDocFrontMatter sidebar_label', () => {
|
||||||
testField({
|
testField({
|
||||||
fieldName: 'sidebar_label',
|
prefix: 'sidebar_label',
|
||||||
validFrontMatters: [{sidebar_label: 'Awesome docs'}],
|
validFrontMatters: [{sidebar_label: 'Awesome docs'}],
|
||||||
invalidFrontMatters: [[{sidebar_label: ''}, 'is not allowed to be empty']],
|
invalidFrontMatters: [[{sidebar_label: ''}, 'is not allowed to be empty']],
|
||||||
});
|
});
|
||||||
|
@ -194,7 +192,7 @@ describe('validateDocFrontMatter sidebar_label', () => {
|
||||||
|
|
||||||
describe('validateDocFrontMatter sidebar_position', () => {
|
describe('validateDocFrontMatter sidebar_position', () => {
|
||||||
testField({
|
testField({
|
||||||
fieldName: 'sidebar_position',
|
prefix: 'sidebar_position',
|
||||||
validFrontMatters: [
|
validFrontMatters: [
|
||||||
{sidebar_position: -5},
|
{sidebar_position: -5},
|
||||||
{sidebar_position: -3.5},
|
{sidebar_position: -3.5},
|
||||||
|
@ -212,7 +210,7 @@ describe('validateDocFrontMatter sidebar_position', () => {
|
||||||
|
|
||||||
describe('validateDocFrontMatter custom_edit_url', () => {
|
describe('validateDocFrontMatter custom_edit_url', () => {
|
||||||
testField({
|
testField({
|
||||||
fieldName: 'custom_edit_url',
|
prefix: 'custom_edit_url',
|
||||||
validFrontMatters: [
|
validFrontMatters: [
|
||||||
// See https://github.com/demisto/content-docs/pull/616#issuecomment-827087566
|
// See https://github.com/demisto/content-docs/pull/616#issuecomment-827087566
|
||||||
{custom_edit_url: ''},
|
{custom_edit_url: ''},
|
||||||
|
@ -226,7 +224,7 @@ describe('validateDocFrontMatter custom_edit_url', () => {
|
||||||
|
|
||||||
describe('validateDocFrontMatter parse_number_prefixes', () => {
|
describe('validateDocFrontMatter parse_number_prefixes', () => {
|
||||||
testField({
|
testField({
|
||||||
fieldName: 'parse_number_prefixes',
|
prefix: 'parse_number_prefixes',
|
||||||
validFrontMatters: [
|
validFrontMatters: [
|
||||||
{parse_number_prefixes: true},
|
{parse_number_prefixes: true},
|
||||||
{parse_number_prefixes: false},
|
{parse_number_prefixes: false},
|
||||||
|
@ -245,7 +243,7 @@ describe('validateDocFrontMatter parse_number_prefixes', () => {
|
||||||
|
|
||||||
describe('validateDocFrontMatter tags', () => {
|
describe('validateDocFrontMatter tags', () => {
|
||||||
testField({
|
testField({
|
||||||
fieldName: 'tags',
|
prefix: 'tags',
|
||||||
validFrontMatters: [{}, {tags: undefined}, {tags: ['tag1', 'tag2']}],
|
validFrontMatters: [{}, {tags: undefined}, {tags: ['tag1', 'tag2']}],
|
||||||
convertibleFrontMatter: [[{tags: ['tag1', 42]}, {tags: ['tag1', '42']}]],
|
convertibleFrontMatter: [[{tags: ['tag1', 42]}, {tags: ['tag1', '42']}]],
|
||||||
invalidFrontMatters: [
|
invalidFrontMatters: [
|
||||||
|
@ -263,3 +261,99 @@ describe('validateDocFrontMatter tags', () => {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('validateDocFrontMatter toc_min_heading_level', () => {
|
||||||
|
testField({
|
||||||
|
prefix: 'toc_min_heading_level',
|
||||||
|
validFrontMatters: [
|
||||||
|
{},
|
||||||
|
{toc_min_heading_level: undefined},
|
||||||
|
{toc_min_heading_level: 2},
|
||||||
|
{toc_min_heading_level: 3},
|
||||||
|
{toc_min_heading_level: 4},
|
||||||
|
{toc_min_heading_level: 5},
|
||||||
|
{toc_min_heading_level: 6},
|
||||||
|
],
|
||||||
|
convertibleFrontMatter: [
|
||||||
|
[{toc_min_heading_level: '2'}, {toc_min_heading_level: 2}],
|
||||||
|
],
|
||||||
|
invalidFrontMatters: [
|
||||||
|
[
|
||||||
|
{toc_min_heading_level: 1},
|
||||||
|
'"toc_min_heading_level" must be greater than or equal to 2',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{toc_min_heading_level: 7},
|
||||||
|
'"toc_min_heading_level" must be less than or equal to 6',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{toc_min_heading_level: 'hello'},
|
||||||
|
'"toc_min_heading_level" must be a number',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{toc_min_heading_level: true},
|
||||||
|
'"toc_min_heading_level" must be a number',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateDocFrontMatter toc_max_heading_level', () => {
|
||||||
|
testField({
|
||||||
|
prefix: 'toc_max_heading_level',
|
||||||
|
validFrontMatters: [
|
||||||
|
{},
|
||||||
|
{toc_max_heading_level: undefined},
|
||||||
|
{toc_max_heading_level: 2},
|
||||||
|
{toc_max_heading_level: 3},
|
||||||
|
{toc_max_heading_level: 4},
|
||||||
|
{toc_max_heading_level: 5},
|
||||||
|
{toc_max_heading_level: 6},
|
||||||
|
],
|
||||||
|
convertibleFrontMatter: [
|
||||||
|
[{toc_max_heading_level: '2'}, {toc_max_heading_level: 2}],
|
||||||
|
],
|
||||||
|
invalidFrontMatters: [
|
||||||
|
[
|
||||||
|
{toc_max_heading_level: 1},
|
||||||
|
'"toc_max_heading_level" must be greater than or equal to 2',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{toc_max_heading_level: 7},
|
||||||
|
'"toc_max_heading_level" must be less than or equal to 6',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{toc_max_heading_level: 'hello'},
|
||||||
|
'"toc_max_heading_level" must be a number',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{toc_max_heading_level: true},
|
||||||
|
'"toc_max_heading_level" must be a number',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateDocFrontMatter toc min/max consistency', () => {
|
||||||
|
testField({
|
||||||
|
prefix: 'toc min/max',
|
||||||
|
validFrontMatters: [
|
||||||
|
{},
|
||||||
|
{toc_min_heading_level: undefined, toc_max_heading_level: undefined},
|
||||||
|
{toc_min_heading_level: 2, toc_max_heading_level: 2},
|
||||||
|
{toc_min_heading_level: 2, toc_max_heading_level: 6},
|
||||||
|
{toc_min_heading_level: 2, toc_max_heading_level: 3},
|
||||||
|
{toc_min_heading_level: 3, toc_max_heading_level: 3},
|
||||||
|
],
|
||||||
|
invalidFrontMatters: [
|
||||||
|
[
|
||||||
|
{toc_min_heading_level: 4, toc_max_heading_level: 3},
|
||||||
|
'"toc_min_heading_level" must be less than or equal to ref:toc_max_heading_level',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{toc_min_heading_level: 6, toc_max_heading_level: 2},
|
||||||
|
'"toc_min_heading_level" must be less than or equal to ref:toc_max_heading_level',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
JoiFrontMatter as Joi, // Custom instance for frontmatter
|
JoiFrontMatter as Joi, // Custom instance for frontmatter
|
||||||
URISchema,
|
URISchema,
|
||||||
FrontMatterTagsSchema,
|
FrontMatterTagsSchema,
|
||||||
|
FrontMatterTOCHeadingLevels,
|
||||||
validateFrontMatter,
|
validateFrontMatter,
|
||||||
} from '@docusaurus/utils-validation';
|
} from '@docusaurus/utils-validation';
|
||||||
import {DocFrontMatter} from './types';
|
import {DocFrontMatter} from './types';
|
||||||
|
@ -32,6 +33,7 @@ const DocFrontMatterSchema = Joi.object<DocFrontMatter>({
|
||||||
pagination_label: Joi.string(),
|
pagination_label: Joi.string(),
|
||||||
custom_edit_url: URISchema.allow('', null),
|
custom_edit_url: URISchema.allow('', null),
|
||||||
parse_number_prefixes: Joi.boolean(),
|
parse_number_prefixes: Joi.boolean(),
|
||||||
|
...FrontMatterTOCHeadingLevels,
|
||||||
}).unknown();
|
}).unknown();
|
||||||
|
|
||||||
export function validateDocFrontMatter(
|
export function validateDocFrontMatter(
|
||||||
|
|
|
@ -94,6 +94,8 @@ declare module '@theme/DocItem' {
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
readonly hide_title?: boolean;
|
readonly hide_title?: boolean;
|
||||||
readonly hide_table_of_contents?: boolean;
|
readonly hide_table_of_contents?: boolean;
|
||||||
|
readonly toc_min_heading_level?: number;
|
||||||
|
readonly toc_max_heading_level?: number;
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -220,6 +220,8 @@ export type DocFrontMatter = {
|
||||||
pagination_label?: string;
|
pagination_label?: string;
|
||||||
custom_edit_url?: string | null;
|
custom_edit_url?: string | null;
|
||||||
parse_number_prefixes?: boolean;
|
parse_number_prefixes?: boolean;
|
||||||
|
toc_min_heading_level?: number;
|
||||||
|
toc_max_heading_level?: number;
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -121,6 +121,7 @@ export default function pluginContentPages(
|
||||||
encodePath(fileToPath(relativeSource)),
|
encodePath(fileToPath(relativeSource)),
|
||||||
]);
|
]);
|
||||||
if (isMarkdownSource(relativeSource)) {
|
if (isMarkdownSource(relativeSource)) {
|
||||||
|
// TODO: missing frontmatter validation/normalization here
|
||||||
return {
|
return {
|
||||||
type: 'mdx',
|
type: 'mdx',
|
||||||
permalink,
|
permalink,
|
||||||
|
|
|
@ -18,8 +18,11 @@ declare module '@theme/MDXPage' {
|
||||||
readonly title: string;
|
readonly title: string;
|
||||||
readonly description: string;
|
readonly description: string;
|
||||||
readonly wrapperClassName?: string;
|
readonly wrapperClassName?: string;
|
||||||
// eslint-disable-next-line camelcase
|
/* eslint-disable camelcase */
|
||||||
readonly hide_table_of_contents?: string;
|
readonly hide_table_of_contents?: string;
|
||||||
|
readonly toc_min_heading_level?: number;
|
||||||
|
readonly toc_max_heading_level?: number;
|
||||||
|
/* eslint-enable camelcase */
|
||||||
};
|
};
|
||||||
readonly metadata: {readonly permalink: string};
|
readonly metadata: {readonly permalink: string};
|
||||||
readonly toc: readonly TOCItem[];
|
readonly toc: readonly TOCItem[];
|
||||||
|
|
|
@ -91,6 +91,10 @@ describe('themeConfig', () => {
|
||||||
},
|
},
|
||||||
copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc. Built with Docusaurus.`,
|
copyright: `Copyright © ${new Date().getFullYear()} Facebook, Inc. Built with Docusaurus.`,
|
||||||
},
|
},
|
||||||
|
tableOfContents: {
|
||||||
|
minHeadingLevel: 2,
|
||||||
|
maxHeadingLevel: 5,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
expect(testValidateThemeConfig(userConfig)).toEqual({
|
expect(testValidateThemeConfig(userConfig)).toEqual({
|
||||||
...DEFAULT_CONFIG,
|
...DEFAULT_CONFIG,
|
||||||
|
@ -453,3 +457,131 @@ describe('themeConfig', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('themeConfig tableOfContents', () => {
|
||||||
|
test('toc undefined', () => {
|
||||||
|
const tableOfContents = undefined;
|
||||||
|
expect(testValidateThemeConfig({tableOfContents})).toEqual({
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
tableOfContents: {
|
||||||
|
minHeadingLevel: DEFAULT_CONFIG.tableOfContents.minHeadingLevel,
|
||||||
|
maxHeadingLevel: DEFAULT_CONFIG.tableOfContents.maxHeadingLevel,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toc empty', () => {
|
||||||
|
const tableOfContents = {};
|
||||||
|
expect(testValidateThemeConfig({tableOfContents})).toEqual({
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
tableOfContents: {
|
||||||
|
minHeadingLevel: DEFAULT_CONFIG.tableOfContents.minHeadingLevel,
|
||||||
|
maxHeadingLevel: DEFAULT_CONFIG.tableOfContents.maxHeadingLevel,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toc with min', () => {
|
||||||
|
const tableOfContents = {
|
||||||
|
minHeadingLevel: 3,
|
||||||
|
};
|
||||||
|
expect(testValidateThemeConfig({tableOfContents})).toEqual({
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
tableOfContents: {
|
||||||
|
minHeadingLevel: 3,
|
||||||
|
maxHeadingLevel: DEFAULT_CONFIG.tableOfContents.maxHeadingLevel,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toc with max', () => {
|
||||||
|
const tableOfContents = {
|
||||||
|
maxHeadingLevel: 5,
|
||||||
|
};
|
||||||
|
expect(testValidateThemeConfig({tableOfContents})).toEqual({
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
tableOfContents: {
|
||||||
|
minHeadingLevel: DEFAULT_CONFIG.tableOfContents.minHeadingLevel,
|
||||||
|
maxHeadingLevel: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toc with min 2.5', () => {
|
||||||
|
const tableOfContents = {
|
||||||
|
minHeadingLevel: 2.5,
|
||||||
|
};
|
||||||
|
expect(() =>
|
||||||
|
testValidateThemeConfig({tableOfContents}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"\\"tableOfContents.minHeadingLevel\\" must be an integer"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toc with max 2.5', () => {
|
||||||
|
const tableOfContents = {
|
||||||
|
maxHeadingLevel: 2.5,
|
||||||
|
};
|
||||||
|
expect(() =>
|
||||||
|
testValidateThemeConfig({tableOfContents}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"\\"tableOfContents.maxHeadingLevel\\" must be an integer"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toc with min 1', () => {
|
||||||
|
const tableOfContents = {
|
||||||
|
minHeadingLevel: 1,
|
||||||
|
};
|
||||||
|
expect(() =>
|
||||||
|
testValidateThemeConfig({tableOfContents}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"\\"tableOfContents.minHeadingLevel\\" must be greater than or equal to 2"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toc with min 7', () => {
|
||||||
|
const tableOfContents = {
|
||||||
|
minHeadingLevel: 7,
|
||||||
|
};
|
||||||
|
expect(() =>
|
||||||
|
testValidateThemeConfig({tableOfContents}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"\\"tableOfContents.minHeadingLevel\\" must be less than or equal to ref:maxHeadingLevel"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toc with max 1', () => {
|
||||||
|
const tableOfContents = {
|
||||||
|
maxHeadingLevel: 1,
|
||||||
|
};
|
||||||
|
expect(() =>
|
||||||
|
testValidateThemeConfig({tableOfContents}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"\\"tableOfContents.maxHeadingLevel\\" must be greater than or equal to 2"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toc with max 7', () => {
|
||||||
|
const tableOfContents = {
|
||||||
|
maxHeadingLevel: 7,
|
||||||
|
};
|
||||||
|
expect(() =>
|
||||||
|
testValidateThemeConfig({tableOfContents}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"\\"tableOfContents.maxHeadingLevel\\" must be less than or equal to 6"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toc with bad min 5 + max 3', () => {
|
||||||
|
const tableOfContents = {
|
||||||
|
minHeadingLevel: 5,
|
||||||
|
maxHeadingLevel: 3,
|
||||||
|
};
|
||||||
|
expect(() =>
|
||||||
|
testValidateThemeConfig({tableOfContents}),
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
|
`"\\"tableOfContents.minHeadingLevel\\" must be less than or equal to ref:maxHeadingLevel"`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -7,10 +7,8 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import Layout from '@theme/Layout';
|
import Layout from '@theme/Layout';
|
||||||
import BlogSidebar from '@theme/BlogSidebar';
|
import BlogSidebar from '@theme/BlogSidebar';
|
||||||
import TOC from '@theme/TOC';
|
|
||||||
|
|
||||||
import type {Props} from '@theme/BlogLayout';
|
import type {Props} from '@theme/BlogLayout';
|
||||||
|
|
||||||
|
@ -36,11 +34,7 @@ function BlogLayout(props: Props): JSX.Element {
|
||||||
itemType="http://schema.org/Blog">
|
itemType="http://schema.org/Blog">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
{toc && (
|
{toc && <div className="col col--2">{toc}</div>}
|
||||||
<div className="col col--2">
|
|
||||||
<TOC toc={toc} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -12,10 +12,16 @@ import BlogPostItem from '@theme/BlogPostItem';
|
||||||
import BlogPostPaginator from '@theme/BlogPostPaginator';
|
import BlogPostPaginator from '@theme/BlogPostPaginator';
|
||||||
import type {Props} from '@theme/BlogPostPage';
|
import type {Props} from '@theme/BlogPostPage';
|
||||||
import {ThemeClassNames} from '@docusaurus/theme-common';
|
import {ThemeClassNames} from '@docusaurus/theme-common';
|
||||||
|
import TOC from '@theme/TOC';
|
||||||
|
|
||||||
function BlogPostPage(props: Props): JSX.Element {
|
function BlogPostPage(props: Props): JSX.Element {
|
||||||
const {content: BlogPostContents, sidebar} = props;
|
const {content: BlogPostContents, sidebar} = props;
|
||||||
const {frontMatter, assets, metadata} = BlogPostContents;
|
const {
|
||||||
|
// TODO this frontmatter is not validated/normalized, it's the raw user-provided one. We should expose normalized one too!
|
||||||
|
frontMatter,
|
||||||
|
assets,
|
||||||
|
metadata,
|
||||||
|
} = BlogPostContents;
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
@ -25,7 +31,12 @@ function BlogPostPage(props: Props): JSX.Element {
|
||||||
tags,
|
tags,
|
||||||
authors,
|
authors,
|
||||||
} = metadata;
|
} = metadata;
|
||||||
const {hide_table_of_contents: hideTableOfContents, keywords} = frontMatter;
|
const {
|
||||||
|
hide_table_of_contents: hideTableOfContents,
|
||||||
|
keywords,
|
||||||
|
toc_min_heading_level: tocMinHeadingLevel,
|
||||||
|
toc_max_heading_level: tocMaxHeadingLevel,
|
||||||
|
} = frontMatter;
|
||||||
|
|
||||||
const image = assets.image ?? frontMatter.image;
|
const image = assets.image ?? frontMatter.image;
|
||||||
|
|
||||||
|
@ -35,9 +46,15 @@ function BlogPostPage(props: Props): JSX.Element {
|
||||||
pageClassName={ThemeClassNames.page.blogPostPage}
|
pageClassName={ThemeClassNames.page.blogPostPage}
|
||||||
sidebar={sidebar}
|
sidebar={sidebar}
|
||||||
toc={
|
toc={
|
||||||
!hideTableOfContents && BlogPostContents.toc
|
!hideTableOfContents &&
|
||||||
? BlogPostContents.toc
|
BlogPostContents.toc &&
|
||||||
: undefined
|
BlogPostContents.toc.length > 0 ? (
|
||||||
|
<TOC
|
||||||
|
toc={BlogPostContents.toc}
|
||||||
|
minHeadingLevel={tocMinHeadingLevel}
|
||||||
|
maxHeadingLevel={tocMaxHeadingLevel}
|
||||||
|
/>
|
||||||
|
) : undefined
|
||||||
}>
|
}>
|
||||||
<Seo
|
<Seo
|
||||||
// TODO refactor needed: it's a bit annoying but Seo MUST be inside BlogLayout
|
// TODO refactor needed: it's a bit annoying but Seo MUST be inside BlogLayout
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import useWindowSize from '@theme/hooks/useWindowSize';
|
import useWindowSize from '@theme/hooks/useWindowSize';
|
||||||
import DocPaginator from '@theme/DocPaginator';
|
import DocPaginator from '@theme/DocPaginator';
|
||||||
import DocVersionBanner from '@theme/DocVersionBanner';
|
import DocVersionBanner from '@theme/DocVersionBanner';
|
||||||
|
@ -17,7 +16,6 @@ import DocItemFooter from '@theme/DocItemFooter';
|
||||||
import TOC from '@theme/TOC';
|
import TOC from '@theme/TOC';
|
||||||
import TOCCollapsible from '@theme/TOCCollapsible';
|
import TOCCollapsible from '@theme/TOCCollapsible';
|
||||||
import {MainHeading} from '@theme/Heading';
|
import {MainHeading} from '@theme/Heading';
|
||||||
|
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
import {ThemeClassNames} from '@docusaurus/theme-common';
|
import {ThemeClassNames} from '@docusaurus/theme-common';
|
||||||
|
|
||||||
|
@ -29,6 +27,8 @@ export default function DocItem(props: Props): JSX.Element {
|
||||||
keywords,
|
keywords,
|
||||||
hide_title: hideTitle,
|
hide_title: hideTitle,
|
||||||
hide_table_of_contents: hideTableOfContents,
|
hide_table_of_contents: hideTableOfContents,
|
||||||
|
toc_min_heading_level: tocMinHeadingLevel,
|
||||||
|
toc_max_heading_level: tocMaxHeadingLevel,
|
||||||
} = frontMatter;
|
} = frontMatter;
|
||||||
const {description, title} = metadata;
|
const {description, title} = metadata;
|
||||||
|
|
||||||
|
@ -71,6 +71,8 @@ export default function DocItem(props: Props): JSX.Element {
|
||||||
{canRenderTOC && (
|
{canRenderTOC && (
|
||||||
<TOCCollapsible
|
<TOCCollapsible
|
||||||
toc={DocContent.toc}
|
toc={DocContent.toc}
|
||||||
|
minHeadingLevel={tocMinHeadingLevel}
|
||||||
|
maxHeadingLevel={tocMaxHeadingLevel}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
ThemeClassNames.docs.docTocMobile,
|
ThemeClassNames.docs.docTocMobile,
|
||||||
styles.tocMobile,
|
styles.tocMobile,
|
||||||
|
@ -100,6 +102,8 @@ export default function DocItem(props: Props): JSX.Element {
|
||||||
<div className="col col--3">
|
<div className="col col--3">
|
||||||
<TOC
|
<TOC
|
||||||
toc={DocContent.toc}
|
toc={DocContent.toc}
|
||||||
|
minHeadingLevel={tocMinHeadingLevel}
|
||||||
|
maxHeadingLevel={tocMaxHeadingLevel}
|
||||||
className={ThemeClassNames.docs.docTocDesktop}
|
className={ThemeClassNames.docs.docTocDesktop}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -18,7 +18,11 @@ import styles from './styles.module.css';
|
||||||
|
|
||||||
function MDXPage(props: Props): JSX.Element {
|
function MDXPage(props: Props): JSX.Element {
|
||||||
const {content: MDXPageContent} = props;
|
const {content: MDXPageContent} = props;
|
||||||
const {frontMatter, metadata} = MDXPageContent;
|
const {
|
||||||
|
// TODO this frontmatter is not validated/normalized, it's the raw user-provided one. We should expose normalized one too!
|
||||||
|
frontMatter,
|
||||||
|
metadata,
|
||||||
|
} = MDXPageContent;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
|
@ -44,7 +48,11 @@ function MDXPage(props: Props): JSX.Element {
|
||||||
</div>
|
</div>
|
||||||
{!hideTableOfContents && MDXPageContent.toc && (
|
{!hideTableOfContents && MDXPageContent.toc && (
|
||||||
<div className="col col--2">
|
<div className="col col--2">
|
||||||
<TOC toc={MDXPageContent.toc} />
|
<TOC
|
||||||
|
toc={MDXPageContent.toc}
|
||||||
|
minHeadingLevel={frontMatter.toc_min_heading_level}
|
||||||
|
maxHeadingLevel={frontMatter.toc_max_heading_level}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,53 +7,23 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import useTOCHighlight, {
|
import type {TOCProps} from '@theme/TOC';
|
||||||
Params as TOCHighlightParams,
|
import TOCItems from '@theme/TOCItems';
|
||||||
} from '@theme/hooks/useTOCHighlight';
|
|
||||||
import type {TOCProps, TOCHeadingsProps} from '@theme/TOC';
|
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
const LINK_CLASS_NAME = 'table-of-contents__link';
|
// Using a custom className
|
||||||
|
// This prevents TOC highlighting to highlight TOCInline/TOCCollapsible by mistake
|
||||||
|
const LINK_CLASS_NAME = 'table-of-contents__link toc-highlight';
|
||||||
|
const LINK_ACTIVE_CLASS_NAME = 'table-of-contents__link--active';
|
||||||
|
|
||||||
const TOC_HIGHLIGHT_PARAMS: TOCHighlightParams = {
|
function TOC({className, ...props}: TOCProps): JSX.Element {
|
||||||
linkClassName: LINK_CLASS_NAME,
|
|
||||||
linkActiveClassName: 'table-of-contents__link--active',
|
|
||||||
};
|
|
||||||
|
|
||||||
/* eslint-disable jsx-a11y/control-has-associated-label */
|
|
||||||
export function TOCHeadings({
|
|
||||||
toc,
|
|
||||||
isChild,
|
|
||||||
}: TOCHeadingsProps): JSX.Element | null {
|
|
||||||
if (!toc.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<ul
|
<div className={clsx(styles.tableOfContents, 'thin-scrollbar', className)}>
|
||||||
className={
|
<TOCItems
|
||||||
isChild ? '' : 'table-of-contents table-of-contents__left-border'
|
{...props}
|
||||||
}>
|
linkClassName={LINK_CLASS_NAME}
|
||||||
{toc.map((heading) => (
|
linkActiveClassName={LINK_ACTIVE_CLASS_NAME}
|
||||||
<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}}
|
|
||||||
/>
|
/>
|
||||||
<TOCHeadings isChild toc={heading.children} />
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TOC({toc}: TOCProps): JSX.Element {
|
|
||||||
useTOCHighlight(TOC_HIGHLIGHT_PARAMS);
|
|
||||||
return (
|
|
||||||
<div className={clsx(styles.tableOfContents, 'thin-scrollbar')}>
|
|
||||||
<TOCHeadings toc={toc} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,14 @@ import clsx from 'clsx';
|
||||||
import Translate from '@docusaurus/Translate';
|
import Translate from '@docusaurus/Translate';
|
||||||
import {useCollapsible, Collapsible} from '@docusaurus/theme-common';
|
import {useCollapsible, Collapsible} from '@docusaurus/theme-common';
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
import {TOCHeadings} from '@theme/TOC';
|
import TOCItems from '@theme/TOCItems';
|
||||||
import type {TOCCollapsibleProps} from '@theme/TOCCollapsible';
|
import type {TOCCollapsibleProps} from '@theme/TOCCollapsible';
|
||||||
|
|
||||||
export default function TOCCollapsible({
|
export default function TOCCollapsible({
|
||||||
toc,
|
toc,
|
||||||
className,
|
className,
|
||||||
|
minHeadingLevel,
|
||||||
|
maxHeadingLevel,
|
||||||
}: TOCCollapsibleProps): JSX.Element {
|
}: TOCCollapsibleProps): JSX.Element {
|
||||||
const {collapsed, toggleCollapsed} = useCollapsible({
|
const {collapsed, toggleCollapsed} = useCollapsible({
|
||||||
initialState: true,
|
initialState: true,
|
||||||
|
@ -45,7 +47,11 @@ export default function TOCCollapsible({
|
||||||
lazy
|
lazy
|
||||||
className={styles.tocCollapsibleContent}
|
className={styles.tocCollapsibleContent}
|
||||||
collapsed={collapsed}>
|
collapsed={collapsed}>
|
||||||
<TOCHeadings toc={toc} />
|
<TOCItems
|
||||||
|
toc={toc}
|
||||||
|
minHeadingLevel={minHeadingLevel}
|
||||||
|
maxHeadingLevel={maxHeadingLevel}
|
||||||
|
/>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,40 +9,22 @@ import React from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type {TOCInlineProps} from '@theme/TOCInline';
|
import type {TOCInlineProps} from '@theme/TOCInline';
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
import {TOCItem} from '@docusaurus/types';
|
import TOCItems from '@theme/TOCItems';
|
||||||
|
|
||||||
/* eslint-disable jsx-a11y/control-has-associated-label */
|
function TOCInline({
|
||||||
function HeadingsInline({
|
|
||||||
toc,
|
toc,
|
||||||
isChild,
|
minHeadingLevel,
|
||||||
}: {
|
maxHeadingLevel,
|
||||||
toc: readonly TOCItem[];
|
}: TOCInlineProps): JSX.Element {
|
||||||
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}`}
|
|
||||||
// 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}: TOCInlineProps): JSX.Element {
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.tableOfContentsInline)}>
|
<div className={clsx(styles.tableOfContentsInline)}>
|
||||||
<HeadingsInline toc={toc} />
|
<TOCItems
|
||||||
|
toc={toc}
|
||||||
|
minHeadingLevel={minHeadingLevel}
|
||||||
|
maxHeadingLevel={maxHeadingLevel}
|
||||||
|
className="table-of-contents"
|
||||||
|
linkClassName=""
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
/**
|
||||||
|
* 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, {useMemo} from 'react';
|
||||||
|
import type {TOCItemsProps} from '@theme/TOCItems';
|
||||||
|
import {TOCItem} from '@docusaurus/types';
|
||||||
|
import {
|
||||||
|
TOCHighlightConfig,
|
||||||
|
useThemeConfig,
|
||||||
|
useTOCFilter,
|
||||||
|
useTOCHighlight,
|
||||||
|
} from '@docusaurus/theme-common';
|
||||||
|
|
||||||
|
// Recursive component rendering the toc tree
|
||||||
|
/* eslint-disable jsx-a11y/control-has-associated-label */
|
||||||
|
function TOCItemList({
|
||||||
|
toc,
|
||||||
|
className = 'table-of-contents table-of-contents__left-border',
|
||||||
|
linkClassName = 'table-of-contents__link',
|
||||||
|
isChild,
|
||||||
|
}: {
|
||||||
|
readonly toc: readonly TOCItem[];
|
||||||
|
readonly className: string;
|
||||||
|
readonly linkClassName: string;
|
||||||
|
readonly isChild?: boolean;
|
||||||
|
}): JSX.Element | null {
|
||||||
|
if (!toc.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ul className={isChild ? '' : className}>
|
||||||
|
{toc.map((heading) => (
|
||||||
|
<li key={heading.id}>
|
||||||
|
<a
|
||||||
|
href={`#${heading.id}`}
|
||||||
|
className={linkClassName}
|
||||||
|
// Developer provided the HTML, so assume it's safe.
|
||||||
|
// eslint-disable-next-line react/no-danger
|
||||||
|
dangerouslySetInnerHTML={{__html: heading.value}}
|
||||||
|
/>
|
||||||
|
<TOCItemList
|
||||||
|
isChild
|
||||||
|
toc={heading.children}
|
||||||
|
className={className}
|
||||||
|
linkClassName={linkClassName}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TOCItems({
|
||||||
|
toc,
|
||||||
|
className = 'table-of-contents table-of-contents__left-border',
|
||||||
|
linkClassName = 'table-of-contents__link',
|
||||||
|
linkActiveClassName = undefined,
|
||||||
|
minHeadingLevel: minHeadingLevelOption,
|
||||||
|
maxHeadingLevel: maxHeadingLevelOption,
|
||||||
|
...props
|
||||||
|
}: TOCItemsProps): JSX.Element | null {
|
||||||
|
const themeConfig = useThemeConfig();
|
||||||
|
|
||||||
|
const minHeadingLevel =
|
||||||
|
minHeadingLevelOption ?? themeConfig.tableOfContents.minHeadingLevel;
|
||||||
|
const maxHeadingLevel =
|
||||||
|
maxHeadingLevelOption ?? themeConfig.tableOfContents.maxHeadingLevel;
|
||||||
|
|
||||||
|
const tocFiltered = useTOCFilter({toc, minHeadingLevel, maxHeadingLevel});
|
||||||
|
|
||||||
|
const tocHighlightConfig: TOCHighlightConfig | undefined = useMemo(() => {
|
||||||
|
if (linkClassName && linkActiveClassName) {
|
||||||
|
return {
|
||||||
|
linkClassName,
|
||||||
|
linkActiveClassName,
|
||||||
|
minHeadingLevel,
|
||||||
|
maxHeadingLevel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [linkClassName, linkActiveClassName, minHeadingLevel, maxHeadingLevel]);
|
||||||
|
useTOCHighlight(tocHighlightConfig);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TOCItemList
|
||||||
|
toc={tocFiltered}
|
||||||
|
className={className}
|
||||||
|
linkClassName={linkClassName}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
39
packages/docusaurus-theme-classic/src/types.d.ts
vendored
39
packages/docusaurus-theme-classic/src/types.d.ts
vendored
|
@ -75,13 +75,13 @@ declare module '@theme/BlogPostPaginator' {
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/BlogLayout' {
|
declare module '@theme/BlogLayout' {
|
||||||
|
import type {ReactNode} from 'react';
|
||||||
import type {Props as LayoutProps} from '@theme/Layout';
|
import type {Props as LayoutProps} from '@theme/Layout';
|
||||||
import type {BlogSidebar} from '@theme/BlogSidebar';
|
import type {BlogSidebar} from '@theme/BlogSidebar';
|
||||||
import type {TOCItem} from '@docusaurus/types';
|
|
||||||
|
|
||||||
export type Props = LayoutProps & {
|
export type Props = LayoutProps & {
|
||||||
readonly sidebar?: BlogSidebar;
|
readonly sidebar?: BlogSidebar;
|
||||||
readonly toc?: readonly TOCItem[];
|
readonly toc?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BlogLayout: (props: Props) => JSX.Element;
|
const BlogLayout: (props: Props) => JSX.Element;
|
||||||
|
@ -255,14 +255,6 @@ declare module '@theme/hooks/useThemeContext' {
|
||||||
export default function useThemeContext(): ThemeContextProps;
|
export default function useThemeContext(): ThemeContextProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/hooks/useTOCHighlight' {
|
|
||||||
export type Params = {
|
|
||||||
linkClassName: string;
|
|
||||||
linkActiveClassName: string;
|
|
||||||
};
|
|
||||||
export default function useTOCHighlight(params: Params): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module '@theme/hooks/useUserPreferencesContext' {
|
declare module '@theme/hooks/useUserPreferencesContext' {
|
||||||
export type UserPreferencesContextProps = {
|
export type UserPreferencesContextProps = {
|
||||||
tabGroupChoices: {readonly [groupId: string]: string};
|
tabGroupChoices: {readonly [groupId: string]: string};
|
||||||
|
@ -578,17 +570,38 @@ declare module '@theme/ThemeProvider' {
|
||||||
export default ThemeProvider;
|
export default ThemeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '@theme/TOCItems' {
|
||||||
|
import type {TOCItem} from '@docusaurus/types';
|
||||||
|
|
||||||
|
export type TOCItemsProps = {
|
||||||
|
readonly toc: readonly TOCItem[];
|
||||||
|
readonly minHeadingLevel?: number;
|
||||||
|
readonly maxHeadingLevel?: number;
|
||||||
|
readonly className?: string;
|
||||||
|
readonly linkClassName?: string;
|
||||||
|
readonly linkActiveClassName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TOCItems(props: TOCItemsProps): JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@theme/TOC' {
|
declare module '@theme/TOC' {
|
||||||
import type {TOCItem} from '@docusaurus/types';
|
import type {TOCItem} from '@docusaurus/types';
|
||||||
|
|
||||||
|
// minHeadingLevel only exists as a per-doc option,
|
||||||
|
// and won't have a default set by Joi. See TOC, TOCInline,
|
||||||
|
// TOCCollapsible for examples
|
||||||
export type TOCProps = {
|
export type TOCProps = {
|
||||||
readonly toc: readonly TOCItem[];
|
readonly toc: readonly TOCItem[];
|
||||||
|
readonly minHeadingLevel?: number;
|
||||||
|
readonly maxHeadingLevel?: number;
|
||||||
readonly className?: string;
|
readonly className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOCHeadingsProps = {
|
export type TOCHeadingsProps = {
|
||||||
readonly toc: readonly TOCItem[];
|
readonly toc: readonly TOCItem[];
|
||||||
readonly isChild?: boolean;
|
readonly minHeadingLevel?: number;
|
||||||
|
readonly maxHeadingLevel?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TOCHeadings: (props: TOCHeadingsProps) => JSX.Element;
|
export const TOCHeadings: (props: TOCHeadingsProps) => JSX.Element;
|
||||||
|
@ -602,6 +615,8 @@ declare module '@theme/TOCInline' {
|
||||||
|
|
||||||
export type TOCInlineProps = {
|
export type TOCInlineProps = {
|
||||||
readonly toc: readonly TOCItem[];
|
readonly toc: readonly TOCItem[];
|
||||||
|
readonly minHeadingLevel?: number;
|
||||||
|
readonly maxHeadingLevel?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TOCInline: (props: TOCInlineProps) => JSX.Element;
|
const TOCInline: (props: TOCInlineProps) => JSX.Element;
|
||||||
|
@ -613,6 +628,8 @@ declare module '@theme/TOCCollapsible' {
|
||||||
|
|
||||||
export type TOCCollapsibleProps = {
|
export type TOCCollapsibleProps = {
|
||||||
readonly className?: string;
|
readonly className?: string;
|
||||||
|
readonly minHeadingLevel?: number;
|
||||||
|
readonly maxHeadingLevel?: number;
|
||||||
readonly toc: readonly TOCItem[];
|
readonly toc: readonly TOCItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,10 @@ const DEFAULT_CONFIG = {
|
||||||
items: [],
|
items: [],
|
||||||
},
|
},
|
||||||
hideableSidebar: false,
|
hideableSidebar: false,
|
||||||
|
tableOfContents: {
|
||||||
|
minHeadingLevel: 2,
|
||||||
|
maxHeadingLevel: 3,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
|
exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
|
||||||
|
|
||||||
|
@ -329,6 +333,24 @@ const ThemeConfigSchema = Joi.object({
|
||||||
'any.unknown':
|
'any.unknown':
|
||||||
'The themeConfig.sidebarCollapsible has been moved to docs plugin options. See: https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-docs',
|
'The themeConfig.sidebarCollapsible has been moved to docs plugin options. See: https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-docs',
|
||||||
}),
|
}),
|
||||||
|
tableOfContents: Joi.object({
|
||||||
|
minHeadingLevel: Joi.number()
|
||||||
|
.default(DEFAULT_CONFIG.tableOfContents.minHeadingLevel)
|
||||||
|
.when('maxHeadingLevel', {
|
||||||
|
is: Joi.exist(),
|
||||||
|
then: Joi.number()
|
||||||
|
.integer()
|
||||||
|
.min(2)
|
||||||
|
.max(6)
|
||||||
|
.max(Joi.ref('maxHeadingLevel')),
|
||||||
|
otherwise: Joi.number().integer().min(2).max(6),
|
||||||
|
}),
|
||||||
|
maxHeadingLevel: Joi.number()
|
||||||
|
.integer()
|
||||||
|
.min(2)
|
||||||
|
.max(6)
|
||||||
|
.default(DEFAULT_CONFIG.tableOfContents.maxHeadingLevel),
|
||||||
|
}).default(DEFAULT_CONFIG.tableOfContents),
|
||||||
});
|
});
|
||||||
|
|
||||||
export {ThemeConfigSchema};
|
export {ThemeConfigSchema};
|
||||||
|
|
|
@ -73,3 +73,8 @@ export {translateTagsPageTitle, listTagsByLetters} from './utils/tagsUtils';
|
||||||
export type {TagLetterEntry} from './utils/tagsUtils';
|
export type {TagLetterEntry} from './utils/tagsUtils';
|
||||||
|
|
||||||
export {useHistoryPopHandler} from './utils/historyUtils';
|
export {useHistoryPopHandler} from './utils/historyUtils';
|
||||||
|
|
||||||
|
export {default as useTOCHighlight} from './utils/useTOCHighlight';
|
||||||
|
export type {TOCHighlightConfig} from './utils/useTOCHighlight';
|
||||||
|
|
||||||
|
export {useTOCFilter} from './utils/tocUtils';
|
||||||
|
|
|
@ -0,0 +1,197 @@
|
||||||
|
/**
|
||||||
|
* 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 {TOCItem} from '@docusaurus/types';
|
||||||
|
import {filterTOC} from '../tocUtils';
|
||||||
|
|
||||||
|
describe('filterTOC', () => {
|
||||||
|
test('filter a toc with all heading levels', () => {
|
||||||
|
const toc: TOCItem[] = [
|
||||||
|
{
|
||||||
|
id: 'alpha',
|
||||||
|
level: 1,
|
||||||
|
value: 'alpha',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'bravo',
|
||||||
|
level: 2,
|
||||||
|
value: 'Bravo',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'charlie',
|
||||||
|
level: 3,
|
||||||
|
value: 'Charlie',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'delta',
|
||||||
|
level: 4,
|
||||||
|
value: 'Delta',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'echo',
|
||||||
|
level: 5,
|
||||||
|
value: 'Echo',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'foxtrot',
|
||||||
|
level: 6,
|
||||||
|
value: 'Foxtrot',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(filterTOC({toc, minHeadingLevel: 2, maxHeadingLevel: 2})).toEqual([
|
||||||
|
{
|
||||||
|
id: 'bravo',
|
||||||
|
level: 2,
|
||||||
|
value: 'Bravo',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(filterTOC({toc, minHeadingLevel: 3, maxHeadingLevel: 3})).toEqual([
|
||||||
|
{
|
||||||
|
id: 'charlie',
|
||||||
|
level: 3,
|
||||||
|
value: 'Charlie',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(filterTOC({toc, minHeadingLevel: 2, maxHeadingLevel: 3})).toEqual([
|
||||||
|
{
|
||||||
|
id: 'bravo',
|
||||||
|
level: 2,
|
||||||
|
value: 'Bravo',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'charlie',
|
||||||
|
level: 3,
|
||||||
|
value: 'Charlie',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(filterTOC({toc, minHeadingLevel: 2, maxHeadingLevel: 4})).toEqual([
|
||||||
|
{
|
||||||
|
id: 'bravo',
|
||||||
|
level: 2,
|
||||||
|
value: 'Bravo',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'charlie',
|
||||||
|
level: 3,
|
||||||
|
value: 'Charlie',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'delta',
|
||||||
|
level: 4,
|
||||||
|
value: 'Delta',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// It's not 100% clear exactly how the TOC should behave under weird heading levels provided by the user
|
||||||
|
// Adding a test so that behavior stays the same over time
|
||||||
|
test('filter invalid heading levels (but possible) TOC', () => {
|
||||||
|
const toc: TOCItem[] = [
|
||||||
|
{
|
||||||
|
id: 'charlie',
|
||||||
|
level: 3,
|
||||||
|
value: 'Charlie',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bravo',
|
||||||
|
level: 2,
|
||||||
|
value: 'Bravo',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'delta',
|
||||||
|
level: 4,
|
||||||
|
value: 'Delta',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(filterTOC({toc, minHeadingLevel: 2, maxHeadingLevel: 2})).toEqual([
|
||||||
|
{
|
||||||
|
id: 'bravo',
|
||||||
|
level: 2,
|
||||||
|
value: 'Bravo',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(filterTOC({toc, minHeadingLevel: 3, maxHeadingLevel: 3})).toEqual([
|
||||||
|
{
|
||||||
|
id: 'charlie',
|
||||||
|
level: 3,
|
||||||
|
value: 'Charlie',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(filterTOC({toc, minHeadingLevel: 4, maxHeadingLevel: 4})).toEqual([
|
||||||
|
{
|
||||||
|
id: 'delta',
|
||||||
|
level: 4,
|
||||||
|
value: 'Delta',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(filterTOC({toc, minHeadingLevel: 2, maxHeadingLevel: 3})).toEqual([
|
||||||
|
{
|
||||||
|
id: 'charlie',
|
||||||
|
level: 3,
|
||||||
|
value: 'Charlie',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bravo',
|
||||||
|
level: 2,
|
||||||
|
value: 'Bravo',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(filterTOC({toc, minHeadingLevel: 3, maxHeadingLevel: 4})).toEqual([
|
||||||
|
{
|
||||||
|
id: 'charlie',
|
||||||
|
level: 3,
|
||||||
|
value: 'Charlie',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'delta',
|
||||||
|
level: 4,
|
||||||
|
value: 'Delta',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
54
packages/docusaurus-theme-common/src/utils/tocUtils.ts
Normal file
54
packages/docusaurus-theme-common/src/utils/tocUtils.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/**
|
||||||
|
* 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 {useMemo} from 'react';
|
||||||
|
import {TOCItem} from '@docusaurus/types';
|
||||||
|
|
||||||
|
type FilterTOCParam = {
|
||||||
|
toc: readonly TOCItem[];
|
||||||
|
minHeadingLevel: number;
|
||||||
|
maxHeadingLevel: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function filterTOC({
|
||||||
|
toc,
|
||||||
|
minHeadingLevel,
|
||||||
|
maxHeadingLevel,
|
||||||
|
}: FilterTOCParam): TOCItem[] {
|
||||||
|
function isValid(item: TOCItem) {
|
||||||
|
return item.level >= minHeadingLevel && item.level <= maxHeadingLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return toc.flatMap((item) => {
|
||||||
|
const filteredChildren = filterTOC({
|
||||||
|
toc: item.children,
|
||||||
|
minHeadingLevel,
|
||||||
|
maxHeadingLevel,
|
||||||
|
});
|
||||||
|
if (isValid(item)) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...item,
|
||||||
|
children: filteredChildren,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
return filteredChildren;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memoize potentially expensive filtering logic
|
||||||
|
export function useTOCFilter({
|
||||||
|
toc,
|
||||||
|
minHeadingLevel,
|
||||||
|
maxHeadingLevel,
|
||||||
|
}: FilterTOCParam): readonly TOCItem[] {
|
||||||
|
return useMemo(() => {
|
||||||
|
return filterTOC({toc, minHeadingLevel, maxHeadingLevel});
|
||||||
|
}, [toc, minHeadingLevel, maxHeadingLevel]);
|
||||||
|
}
|
|
@ -5,9 +5,13 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Params} from '@theme/hooks/useTOCHighlight';
|
|
||||||
import {useEffect, useRef} from 'react';
|
import {useEffect, useRef} from 'react';
|
||||||
import {useThemeConfig} from '@docusaurus/theme-common';
|
import {useThemeConfig} from './useThemeConfig';
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO make the hardcoded theme-classic classnames configurable
|
||||||
|
(or add them to ThemeClassNames?)
|
||||||
|
*/
|
||||||
|
|
||||||
// If the anchor has no height and is just a "marker" in the dom; we'll use the parent (normally the link text) rect boundaries instead
|
// If the anchor has no height and is just a "marker" in the dom; we'll use the parent (normally the link text) rect boundaries instead
|
||||||
function getVisibleBoundingClientRect(element: HTMLElement): DOMRect {
|
function getVisibleBoundingClientRect(element: HTMLElement): DOMRect {
|
||||||
|
@ -25,19 +29,30 @@ function isInViewportTopHalf(boundingRect: DOMRect) {
|
||||||
return boundingRect.top > 0 && boundingRect.bottom < window.innerHeight / 2;
|
return boundingRect.top > 0 && boundingRect.bottom < window.innerHeight / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAnchors() {
|
function getAnchors({
|
||||||
// For toc highlighting, we only consider h2/h3 anchors
|
minHeadingLevel,
|
||||||
const selector = '.anchor.anchor__h2, .anchor.anchor__h3';
|
maxHeadingLevel,
|
||||||
|
}: {
|
||||||
|
minHeadingLevel: number;
|
||||||
|
maxHeadingLevel: number;
|
||||||
|
}) {
|
||||||
|
const selectors = [];
|
||||||
|
for (let i = minHeadingLevel; i <= maxHeadingLevel; i += 1) {
|
||||||
|
selectors.push(`.anchor.anchor__h${i}`);
|
||||||
|
}
|
||||||
|
const selector = selectors.join(', ');
|
||||||
|
|
||||||
return Array.from(document.querySelectorAll(selector)) as HTMLElement[];
|
return Array.from(document.querySelectorAll(selector)) as HTMLElement[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActiveAnchor({
|
function getActiveAnchor(
|
||||||
|
anchors: HTMLElement[],
|
||||||
|
{
|
||||||
anchorTopOffset,
|
anchorTopOffset,
|
||||||
}: {
|
}: {
|
||||||
anchorTopOffset: number;
|
anchorTopOffset: number;
|
||||||
}): Element | null {
|
},
|
||||||
const anchors = getAnchors();
|
): Element | null {
|
||||||
|
|
||||||
// Naming is hard
|
// Naming is hard
|
||||||
// The "nextVisibleAnchor" is the first anchor that appear under the viewport top boundary
|
// The "nextVisibleAnchor" is the first anchor that appear under the viewport top boundary
|
||||||
// Note: it does not mean this anchor is visible yet, but if user continues scrolling down, it will be the first to become visible
|
// Note: it does not mean this anchor is visible yet, but if user continues scrolling down, it will be the first to become visible
|
||||||
|
@ -96,13 +111,30 @@ function useAnchorTopOffsetRef() {
|
||||||
return anchorTopOffsetRef;
|
return anchorTopOffsetRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
function useTOCHighlight(params: Params): void {
|
export type TOCHighlightConfig = {
|
||||||
|
linkClassName: string;
|
||||||
|
linkActiveClassName: string;
|
||||||
|
minHeadingLevel: number;
|
||||||
|
maxHeadingLevel: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function useTOCHighlight(config: TOCHighlightConfig | undefined): void {
|
||||||
const lastActiveLinkRef = useRef<HTMLAnchorElement | undefined>(undefined);
|
const lastActiveLinkRef = useRef<HTMLAnchorElement | undefined>(undefined);
|
||||||
|
|
||||||
const anchorTopOffsetRef = useAnchorTopOffsetRef();
|
const anchorTopOffsetRef = useAnchorTopOffsetRef();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const {linkClassName, linkActiveClassName} = params;
|
if (!config) {
|
||||||
|
// no-op, highlighting is disabled
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
linkClassName,
|
||||||
|
linkActiveClassName,
|
||||||
|
minHeadingLevel,
|
||||||
|
maxHeadingLevel,
|
||||||
|
} = config;
|
||||||
|
|
||||||
function updateLinkActiveClass(link: HTMLAnchorElement, active: boolean) {
|
function updateLinkActiveClass(link: HTMLAnchorElement, active: boolean) {
|
||||||
if (active) {
|
if (active) {
|
||||||
|
@ -118,7 +150,8 @@ function useTOCHighlight(params: Params): void {
|
||||||
|
|
||||||
function updateActiveLink() {
|
function updateActiveLink() {
|
||||||
const links = getLinks(linkClassName);
|
const links = getLinks(linkClassName);
|
||||||
const activeAnchor = getActiveAnchor({
|
const anchors = getAnchors({minHeadingLevel, maxHeadingLevel});
|
||||||
|
const activeAnchor = getActiveAnchor(anchors, {
|
||||||
anchorTopOffset: anchorTopOffsetRef.current,
|
anchorTopOffset: anchorTopOffsetRef.current,
|
||||||
});
|
});
|
||||||
const activeLink = links.find(
|
const activeLink = links.find(
|
||||||
|
@ -139,7 +172,7 @@ function useTOCHighlight(params: Params): void {
|
||||||
document.removeEventListener('scroll', updateActiveLink);
|
document.removeEventListener('scroll', updateActiveLink);
|
||||||
document.removeEventListener('resize', updateActiveLink);
|
document.removeEventListener('resize', updateActiveLink);
|
||||||
};
|
};
|
||||||
}, [params, anchorTopOffsetRef]);
|
}, [config, anchorTopOffsetRef]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useTOCHighlight;
|
export default useTOCHighlight;
|
|
@ -85,6 +85,11 @@ export type Footer = {
|
||||||
links: FooterLinks[];
|
links: FooterLinks[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TableOfContents = {
|
||||||
|
minHeadingLevel: number;
|
||||||
|
maxHeadingLevel: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type ThemeConfig = {
|
export type ThemeConfig = {
|
||||||
docs: {
|
docs: {
|
||||||
versionPersistence: DocsVersionPersistence;
|
versionPersistence: DocsVersionPersistence;
|
||||||
|
@ -104,6 +109,7 @@ export type ThemeConfig = {
|
||||||
image?: string;
|
image?: string;
|
||||||
metadatas: Array<Record<string, string>>;
|
metadatas: Array<Record<string, string>>;
|
||||||
sidebarCollapsible: boolean;
|
sidebarCollapsible: boolean;
|
||||||
|
tableOfContents: TableOfContents;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useThemeConfig(): ThemeConfig {
|
export function useThemeConfig(): ThemeConfig {
|
||||||
|
|
1
packages/docusaurus-types/src/index.d.ts
vendored
1
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -421,4 +421,5 @@ export interface TOCItem {
|
||||||
readonly value: string;
|
readonly value: string;
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly children: TOCItem[];
|
readonly children: TOCItem[];
|
||||||
|
readonly level: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,3 +80,14 @@ export const FrontMatterTagsSchema = JoiFrontMatter.array()
|
||||||
'array.base':
|
'array.base':
|
||||||
'{{#label}} does not look like a valid FrontMatter Yaml array.',
|
'{{#label}} does not look like a valid FrontMatter Yaml array.',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const FrontMatterTOCHeadingLevels = {
|
||||||
|
toc_min_heading_level: JoiFrontMatter.number().when('toc_max_heading_level', {
|
||||||
|
is: JoiFrontMatter.exist(),
|
||||||
|
then: JoiFrontMatter.number()
|
||||||
|
.min(2)
|
||||||
|
.max(JoiFrontMatter.ref('toc_max_heading_level')),
|
||||||
|
otherwise: JoiFrontMatter.number().min(2).max(6),
|
||||||
|
}),
|
||||||
|
toc_max_heading_level: JoiFrontMatter.number().min(2).max(6),
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
title: Blog TOC FrontMatter tests
|
||||||
|
authors:
|
||||||
|
- slorber
|
||||||
|
toc_min_heading_level: 2
|
||||||
|
toc_max_heading_level: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- truncate -->
|
||||||
|
|
||||||
|
import Content, {
|
||||||
|
toc as ContentToc,
|
||||||
|
} from '@site/_dogfooding/_partials/toc-tests.md';
|
||||||
|
|
||||||
|
<Content />
|
||||||
|
|
||||||
|
export const toc = ContentToc;
|
12
website/_dogfooding/_docs tests/toc/toc-2-2.mdx
Normal file
12
website/_dogfooding/_docs tests/toc/toc-2-2.mdx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
toc_min_heading_level: 2
|
||||||
|
toc_max_heading_level: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
import Content, {
|
||||||
|
toc as ContentToc,
|
||||||
|
} from '@site/_dogfooding/_partials/toc-tests.md';
|
||||||
|
|
||||||
|
<Content />
|
||||||
|
|
||||||
|
export const toc = ContentToc;
|
12
website/_dogfooding/_docs tests/toc/toc-2-3.mdx
Normal file
12
website/_dogfooding/_docs tests/toc/toc-2-3.mdx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
toc_min_heading_level: 2
|
||||||
|
toc_max_heading_level: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
import Content, {
|
||||||
|
toc as ContentToc,
|
||||||
|
} from '@site/_dogfooding/_partials/toc-tests.md';
|
||||||
|
|
||||||
|
<Content />
|
||||||
|
|
||||||
|
export const toc = ContentToc;
|
12
website/_dogfooding/_docs tests/toc/toc-2-4.mdx
Normal file
12
website/_dogfooding/_docs tests/toc/toc-2-4.mdx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
toc_min_heading_level: 2
|
||||||
|
toc_max_heading_level: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
import Content, {
|
||||||
|
toc as ContentToc,
|
||||||
|
} from '@site/_dogfooding/_partials/toc-tests.md';
|
||||||
|
|
||||||
|
<Content />
|
||||||
|
|
||||||
|
export const toc = ContentToc;
|
12
website/_dogfooding/_docs tests/toc/toc-2-5.mdx
Normal file
12
website/_dogfooding/_docs tests/toc/toc-2-5.mdx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
toc_min_heading_level: 2
|
||||||
|
toc_max_heading_level: 5
|
||||||
|
---
|
||||||
|
|
||||||
|
import Content, {
|
||||||
|
toc as ContentToc,
|
||||||
|
} from '@site/_dogfooding/_partials/toc-tests.md';
|
||||||
|
|
||||||
|
<Content />
|
||||||
|
|
||||||
|
export const toc = ContentToc;
|
12
website/_dogfooding/_docs tests/toc/toc-3-5.mdx
Normal file
12
website/_dogfooding/_docs tests/toc/toc-3-5.mdx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
toc_min_heading_level: 3
|
||||||
|
toc_max_heading_level: 5
|
||||||
|
---
|
||||||
|
|
||||||
|
import Content, {
|
||||||
|
toc as ContentToc,
|
||||||
|
} from '@site/_dogfooding/_partials/toc-tests.md';
|
||||||
|
|
||||||
|
<Content />
|
||||||
|
|
||||||
|
export const toc = ContentToc;
|
12
website/_dogfooding/_docs tests/toc/toc-3-_.mdx
Normal file
12
website/_dogfooding/_docs tests/toc/toc-3-_.mdx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
toc_min_heading_level: 3
|
||||||
|
# toc_max_heading_level:
|
||||||
|
---
|
||||||
|
|
||||||
|
import Content, {
|
||||||
|
toc as ContentToc,
|
||||||
|
} from '@site/_dogfooding/_partials/toc-tests.md';
|
||||||
|
|
||||||
|
<Content />
|
||||||
|
|
||||||
|
export const toc = ContentToc;
|
12
website/_dogfooding/_docs tests/toc/toc-4-5.mdx
Normal file
12
website/_dogfooding/_docs tests/toc/toc-4-5.mdx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
toc_min_heading_level: 4
|
||||||
|
toc_max_heading_level: 5
|
||||||
|
---
|
||||||
|
|
||||||
|
import Content, {
|
||||||
|
toc as ContentToc,
|
||||||
|
} from '@site/_dogfooding/_partials/toc-tests.md';
|
||||||
|
|
||||||
|
<Content />
|
||||||
|
|
||||||
|
export const toc = ContentToc;
|
12
website/_dogfooding/_docs tests/toc/toc-5-5.mdx
Normal file
12
website/_dogfooding/_docs tests/toc/toc-5-5.mdx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
toc_min_heading_level: 5
|
||||||
|
toc_max_heading_level: 5
|
||||||
|
---
|
||||||
|
|
||||||
|
import Content, {
|
||||||
|
toc as ContentToc,
|
||||||
|
} from '@site/_dogfooding/_partials/toc-tests.md';
|
||||||
|
|
||||||
|
<Content />
|
||||||
|
|
||||||
|
export const toc = ContentToc;
|
12
website/_dogfooding/_docs tests/toc/toc-_-5.mdx
Normal file
12
website/_dogfooding/_docs tests/toc/toc-_-5.mdx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
# toc_min_heading_level:
|
||||||
|
toc_max_heading_level: 5
|
||||||
|
---
|
||||||
|
|
||||||
|
import Content, {
|
||||||
|
toc as ContentToc,
|
||||||
|
} from '@site/_dogfooding/_partials/toc-tests.md';
|
||||||
|
|
||||||
|
<Content />
|
||||||
|
|
||||||
|
export const toc = ContentToc;
|
12
website/_dogfooding/_docs tests/toc/toc-_-_.mdx
Normal file
12
website/_dogfooding/_docs tests/toc/toc-_-_.mdx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
# toc_min_heading_level:
|
||||||
|
# toc_max_heading_level:
|
||||||
|
---
|
||||||
|
|
||||||
|
import Content, {
|
||||||
|
toc as ContentToc,
|
||||||
|
} from '@site/_dogfooding/_partials/toc-tests.md';
|
||||||
|
|
||||||
|
<Content />
|
||||||
|
|
||||||
|
export const toc = ContentToc;
|
80
website/_dogfooding/_docs tests/toc/toc-test-bad.mdx
Normal file
80
website/_dogfooding/_docs tests/toc/toc-test-bad.mdx
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
---
|
||||||
|
toc_min_heading_level: 2
|
||||||
|
toc_max_heading_level: 6
|
||||||
|
---
|
||||||
|
|
||||||
|
Test the TOC behavior of a real-world md doc with invalid headings
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
BAD HEADINGS:
|
||||||
|
|
||||||
|
###### lvl 6
|
||||||
|
|
||||||
|
##### lvl 5
|
||||||
|
|
||||||
|
#### lvl 4
|
||||||
|
|
||||||
|
##### lvl 5
|
||||||
|
|
||||||
|
#### lvl 4
|
||||||
|
|
||||||
|
### lvl 3
|
||||||
|
|
||||||
|
## lvl 2
|
||||||
|
|
||||||
|
# lvl 1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
GOOD HEADINGS:
|
||||||
|
|
||||||
|
## lvl 2
|
||||||
|
|
||||||
|
### lvl 3
|
||||||
|
|
||||||
|
#### lvl 4
|
||||||
|
|
||||||
|
##### lvl 5
|
||||||
|
|
||||||
|
###### lvl 6
|
||||||
|
|
||||||
|
## lvl 2
|
||||||
|
|
||||||
|
### lvl 3
|
||||||
|
|
||||||
|
#### lvl 4
|
||||||
|
|
||||||
|
##### lvl 5
|
||||||
|
|
||||||
|
###### lvl 6
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
INLINE:
|
||||||
|
|
||||||
|
```mdx-code-block
|
||||||
|
import BrowserWindow from '@site/src/components/BrowserWindow';
|
||||||
|
|
||||||
|
import TOCInline from '@theme/TOCInline';
|
||||||
|
|
||||||
|
<BrowserWindow>
|
||||||
|
|
||||||
|
<TOCInline toc={toc} minHeadingLevel={2} maxHeadingLevel={6} />
|
||||||
|
|
||||||
|
</BrowserWindow>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
COLLAPSIBLE:
|
||||||
|
|
||||||
|
```mdx-code-block
|
||||||
|
import TOCCollapsible from '@theme/TOCCollapsible';
|
||||||
|
|
||||||
|
<BrowserWindow>
|
||||||
|
|
||||||
|
<TOCCollapsible toc={toc} minHeadingLevel={2} maxHeadingLevel={6} />
|
||||||
|
|
||||||
|
</BrowserWindow>
|
||||||
|
```
|
58
website/_dogfooding/_docs tests/toc/toc-test-good.mdx
Normal file
58
website/_dogfooding/_docs tests/toc/toc-test-good.mdx
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
---
|
||||||
|
toc_min_heading_level: 2
|
||||||
|
toc_max_heading_level: 6
|
||||||
|
---
|
||||||
|
|
||||||
|
Test the TOC behavior of a real-world md doc with valid headings
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## lvl 2
|
||||||
|
|
||||||
|
### lvl 3
|
||||||
|
|
||||||
|
#### lvl 4
|
||||||
|
|
||||||
|
##### lvl 5
|
||||||
|
|
||||||
|
###### lvl 6
|
||||||
|
|
||||||
|
## lvl 2
|
||||||
|
|
||||||
|
### lvl 3
|
||||||
|
|
||||||
|
#### lvl 4
|
||||||
|
|
||||||
|
##### lvl 5
|
||||||
|
|
||||||
|
###### lvl 6
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
INLINE:
|
||||||
|
|
||||||
|
```mdx-code-block
|
||||||
|
import BrowserWindow from '@site/src/components/BrowserWindow';
|
||||||
|
|
||||||
|
import TOCInline from '@theme/TOCInline';
|
||||||
|
|
||||||
|
<BrowserWindow>
|
||||||
|
|
||||||
|
<TOCInline toc={toc} minHeadingLevel={2} maxHeadingLevel={6} />
|
||||||
|
|
||||||
|
</BrowserWindow>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
COLLAPSIBLE:
|
||||||
|
|
||||||
|
```mdx-code-block
|
||||||
|
import TOCCollapsible from '@theme/TOCCollapsible';
|
||||||
|
|
||||||
|
<BrowserWindow>
|
||||||
|
|
||||||
|
<TOCCollapsible toc={toc} minHeadingLevel={2} maxHeadingLevel={6} />
|
||||||
|
|
||||||
|
</BrowserWindow>
|
||||||
|
```
|
12
website/_dogfooding/_pages tests/page-toc-tests.mdx
Normal file
12
website/_dogfooding/_pages tests/page-toc-tests.mdx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
toc_min_heading_level: 2
|
||||||
|
toc_max_heading_level: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
import Content, {
|
||||||
|
toc as ContentToc,
|
||||||
|
} from '@site/_dogfooding/_partials/toc-tests.md';
|
||||||
|
|
||||||
|
<Content />
|
||||||
|
|
||||||
|
export const toc = ContentToc;
|
67
website/_dogfooding/_partials/toc-tests.md
Normal file
67
website/_dogfooding/_partials/toc-tests.md
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
# title
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
## section 1
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
### subsection 1-1
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
#### subsection 1-1-1
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
##### subsection 1-1-1-1
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
###### subsection 1-1-1-1-1
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
###### subsection 1-1-1-1-2
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
##### subsection 1-1-1-2
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
#### subsection 1-1-2
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
### subsection 1-2
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
### subsection 1-3
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
## section 2
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
### subsection 2-1
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
### subsection 2-1
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
## section 3
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
### subsection 3-1
|
||||||
|
|
||||||
|
some text
|
||||||
|
|
||||||
|
### subsection 3-2
|
||||||
|
|
||||||
|
some text
|
|
@ -23,6 +23,16 @@ module.exports = {
|
||||||
label: 'Huge sidebar category',
|
label: 'Huge sidebar category',
|
||||||
items: generateHugeSidebarItems(4),
|
items: generateHugeSidebarItems(4),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'TOC tests',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'autogenerated',
|
||||||
|
dirName: 'toc',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -187,6 +187,8 @@ Accepted fields:
|
||||||
| `tags` | `Tag[]` | `undefined` | A list of strings or objects of two string fields `label` and `permalink` to tag to your post. |
|
| `tags` | `Tag[]` | `undefined` | A list of strings or objects of two string fields `label` and `permalink` to tag to your post. |
|
||||||
| `draft` | `boolean` | `false` | A boolean flag to indicate that the blog post is work-in-progress and therefore should not be published yet. However, draft blog posts will be displayed during development. |
|
| `draft` | `boolean` | `false` | A boolean flag to indicate that the blog post is work-in-progress and therefore should not be published yet. However, draft blog posts will be displayed during development. |
|
||||||
| `hide_table_of_contents` | `boolean` | `false` | Whether to hide the table of contents to the right. |
|
| `hide_table_of_contents` | `boolean` | `false` | Whether to hide the table of contents to the right. |
|
||||||
|
| `toc_min_heading_level` | `number` | `2` | The minimum heading level shown in the table of contents. Must be between 2 and 6 and lower or equal to the max value. |
|
||||||
|
| `toc_max_heading_level` | `number` | `3` | The max heading level shown in the table of contents. Must be between 2 and 6. |
|
||||||
| `keywords` | `string[]` | `undefined` | Keywords meta tag, which will become the `<meta name="keywords" content="keyword1,keyword2,..."/>` in `<head>`, used by search engines. |
|
| `keywords` | `string[]` | `undefined` | Keywords meta tag, which will become the `<meta name="keywords" content="keyword1,keyword2,..."/>` in `<head>`, used by search engines. |
|
||||||
| `description` | `string` | The first line of Markdown content | The description of your document, which will become the `<meta name="description" content="..."/>` and `<meta property="og:description" content="..."/>` in `<head>`, used by search engines. |
|
| `description` | `string` | The first line of Markdown content | The description of your document, which will become the `<meta name="description" content="..."/>` and `<meta property="og:description" content="..."/>` in `<head>`, used by search engines. |
|
||||||
| `image` | `string` | `undefined` | Cover or thumbnail image that will be used when displaying the link to your post. |
|
| `image` | `string` | `undefined` | Cover or thumbnail image that will be used when displaying the link to your post. |
|
||||||
|
|
|
@ -248,6 +248,8 @@ Accepted fields:
|
||||||
| `sidebar_position` | `number` | Default ordering | Controls the position of a doc inside the generated sidebar slice when using `autogenerated` sidebar items. See also [Autogenerated sidebar metadatas](/docs/sidebar#autogenerated-sidebar-metadatas). |
|
| `sidebar_position` | `number` | Default ordering | Controls the position of a doc inside the generated sidebar slice when using `autogenerated` sidebar items. See also [Autogenerated sidebar metadatas](/docs/sidebar#autogenerated-sidebar-metadatas). |
|
||||||
| `hide_title` | `boolean` | `false` | Whether to hide the title at the top of the doc. It only hides a title declared through the frontmatter, and have no effect on a Markdown title at the top of your document. |
|
| `hide_title` | `boolean` | `false` | Whether to hide the title at the top of the doc. It only hides a title declared through the frontmatter, and have no effect on a Markdown title at the top of your document. |
|
||||||
| `hide_table_of_contents` | `boolean` | `false` | Whether to hide the table of contents to the right. |
|
| `hide_table_of_contents` | `boolean` | `false` | Whether to hide the table of contents to the right. |
|
||||||
|
| `toc_min_heading_level` | `number` | `2` | The minimum heading level shown in the table of contents. Must be between 2 and 6 and lower or equal to the max value. |
|
||||||
|
| `toc_max_heading_level` | `number` | `3` | The max heading level shown in the table of contents. Must be between 2 and 6. |
|
||||||
| `parse_number_prefixes` | `boolean` | `numberPrefixParser` plugin option | Whether number prefix parsing is disabled on this doc. See also [Using number prefixes](/docs/sidebar#using-number-prefixes). |
|
| `parse_number_prefixes` | `boolean` | `numberPrefixParser` plugin option | Whether number prefix parsing is disabled on this doc. See also [Using number prefixes](/docs/sidebar#using-number-prefixes). |
|
||||||
| `custom_edit_url` | `string` | Computed using the `editUrl` plugin option | The URL for editing this document. |
|
| `custom_edit_url` | `string` | Computed using the `editUrl` plugin option | The URL for editing this document. |
|
||||||
| `keywords` | `string[]` | `undefined` | Keywords meta tag for the document page, for search engines. |
|
| `keywords` | `string[]` | `undefined` | Keywords meta tag for the document page, for search engines. |
|
||||||
|
|
|
@ -757,6 +757,34 @@ module.exports = {
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Table of Contents {#table-of-contents}
|
||||||
|
|
||||||
|
You can adjust the default table of contents via `themeConfig.tableOfContents`.
|
||||||
|
|
||||||
|
<small>
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `minHeadingLevel` | `number` | `2` | The minimum heading level shown in the table of contents. Must be between 2 and 6 and lower or equal to the max value. |
|
||||||
|
| `maxHeadingLevel` | `number` | `3` | Max heading level displayed in the TOC. Should be an integer between 2 and 6. |
|
||||||
|
|
||||||
|
</small>
|
||||||
|
|
||||||
|
Example configuration:
|
||||||
|
|
||||||
|
```js title="docusaurus.config.js"
|
||||||
|
module.exports = {
|
||||||
|
themeConfig: {
|
||||||
|
// highlight-start
|
||||||
|
tableOfContents: {
|
||||||
|
minHeadingLevel: 2,
|
||||||
|
maxHeadingLevel: 5,
|
||||||
|
},
|
||||||
|
// highlight-end
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
## Hooks {#hooks}
|
## Hooks {#hooks}
|
||||||
|
|
||||||
### `useThemeContext` {#usethemecontext}
|
### `useThemeContext` {#usethemecontext}
|
||||||
|
|
|
@ -35,7 +35,9 @@ will show up on the table of contents on the upper right
|
||||||
|
|
||||||
So that your users will know what this page is all about without scrolling down or even without reading too much.
|
So that your users will know what this page is all about without scrolling down or even without reading too much.
|
||||||
|
|
||||||
### Only h2 and h3 will be in the toc
|
### Only h2 and h3 will be in the toc by default.
|
||||||
|
|
||||||
|
You can configure the TOC heading levels either per-document or in the theme configuration.
|
||||||
|
|
||||||
The headers are well-spaced so that the hierarchy is clear.
|
The headers are well-spaced so that the hierarchy is clear.
|
||||||
|
|
||||||
|
@ -67,7 +69,9 @@ will show up on the table of contents on the upper right
|
||||||
|
|
||||||
So that your users will know what this page is all about without scrolling down or even without reading too much.
|
So that your users will know what this page is all about without scrolling down or even without reading too much.
|
||||||
|
|
||||||
<h3>Only h2 and h3 will be in the toc</h3>
|
<h3>Only h2 and h3 will be in the toc by default.</h3>
|
||||||
|
|
||||||
|
You can configure the TOC heading levels either per-document or in the theme configuration.
|
||||||
|
|
||||||
The headers are well-spaced so that the hierarchy is clear.
|
The headers are well-spaced so that the hierarchy is clear.
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,9 @@ But it is also possible to display an inline table of contents directly inside a
|
||||||
|
|
||||||
## Full table of contents {#full-table-of-contents}
|
## Full table of contents {#full-table-of-contents}
|
||||||
|
|
||||||
The `toc` variable is available in any MDX document, and contain all the top level headings of a MDX document.
|
The `toc` variable is available in any MDX document, and contains all the headings of a MDX document.
|
||||||
|
|
||||||
|
By default, only `h2` and `h3` headings are displayed in the TOC. You can change which heading levels are visible by setting `minHeadingLevel` or `maxHeadingLevel`.
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
import TOCInline from '@theme/TOCInline';
|
import TOCInline from '@theme/TOCInline';
|
||||||
|
@ -40,6 +42,7 @@ type TOCItem = {
|
||||||
value: string;
|
value: string;
|
||||||
id: string;
|
id: string;
|
||||||
children: TOCItem[];
|
children: TOCItem[];
|
||||||
|
level: number;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue