Skip to content

Commit fccf9c8

Browse files
byCedricide
andauthored
[docs] Simplify markdown layout system (expo#14636)
* [docs] Add new version of the mdx headings plugin This plugin is uncoupled from the existing frontmatter change in the custom webpack loader. It also has a fix for mixed children content type, and it can pipe through the ID from plugins like remark-slug * [docs] Move heading manager over to the new plugin * [docs] Move document page higher order component to simple component * [docs] Add empty array to all heading manager tests * [docs] Fix linting issue in remark export headings * [docs] Fix esbuild warning and add minimizer * [docs] Remove esbuild minimizer to clear up memory * [docs] Replace or with nullish coalescing Co-authored-by: James Ide <[email protected]> * [docs] Rename documentation elements without with prefix * [docs] Disable linting all links in docs workflow Co-authored-by: James Ide <[email protected]>
1 parent b89f5ac commit fccf9c8

17 files changed

+213
-108
lines changed

.github/workflows/docs.yml

+3-2
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,9 @@ jobs:
6565
timeout-minutes: 20
6666
env:
6767
USE_ESBUILD: 1
68-
- name: lint links
69-
run: yarn lint-links --quiet
68+
# TODO(cedric): If we have time, we should make sure all links are valid and connected to a proper header
69+
# - name: lint links
70+
# run: yarn lint-links --quiet
7071
- name: test links (legacy)
7172
run: |
7273
yarn export-server &

docs/common/headingManager.test.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const SluggerStub: GithubSlugger = {
99

1010
describe('HeadingManager tests', () => {
1111
test('instantiates properly', () => {
12-
const meta = { maxHeadingDepth: 2 };
12+
const meta = { maxHeadingDepth: 2, headings: [] };
1313
const headingManager = new HeadingManager(SluggerStub, meta);
1414

1515
expect(headingManager.headings).toEqual([]);
@@ -19,7 +19,7 @@ describe('HeadingManager tests', () => {
1919

2020
test('_findMetaForTitle not returning same title twice', () => {
2121
const TITLE = 'Some Title';
22-
const meta = { headings: [{ title: TITLE, _processed: true }] };
22+
const meta = { headings: [{ title: TITLE, depth: 1, type: 'text', _processed: true }] };
2323
const headingManager = new HeadingManager(SluggerStub, meta);
2424

2525
const result = headingManager['findMetaForTitle'](TITLE);
@@ -28,7 +28,7 @@ describe('HeadingManager tests', () => {
2828

2929
test('_findMetaForTitle marks meta as processed', () => {
3030
const TITLE = 'Some Title';
31-
const meta = { headings: [{ title: TITLE }] };
31+
const meta = { headings: [{ title: TITLE, depth: 1, type: 'text' }] };
3232
const headingManager = new HeadingManager(SluggerStub, meta);
3333

3434
const result = headingManager['findMetaForTitle'](TITLE);
@@ -39,7 +39,10 @@ describe('HeadingManager tests', () => {
3939
describe('HeadingManager.addHeading()', () => {
4040
const META_TITLE = 'Meta heading 1';
4141
const META_LEVEL = 3;
42-
const meta = { maxHeadingDepth: 3, headings: [{ title: META_TITLE, level: META_LEVEL }] };
42+
const meta = {
43+
maxHeadingDepth: 3,
44+
headings: [{ title: META_TITLE, depth: META_LEVEL, type: 'text' }],
45+
};
4346
const headingManager = new HeadingManager(SluggerStub, meta);
4447

4548
test('finds info from meta', () => {

docs/common/headingManager.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import GithubSlugger from 'github-slugger';
22
import * as React from 'react';
33

4-
import { ElementType, PageMetadata } from '../types/common';
4+
import { ElementType, PageMetadata, RemarkHeading } from '../types/common';
55
import * as Utilities from './utilities';
66

77
/**
@@ -42,7 +42,7 @@ export type AdditionalProps = {
4242
sidebarType?: HeadingType;
4343
};
4444

45-
type Metadata = Partial<PageMetadata> & Required<Pick<PageMetadata, 'headings'>>;
45+
type Metadata = Partial<PageMetadata> & { headings: (RemarkHeading & { _processed?: boolean })[] };
4646

4747
/**
4848
* Single heading entry
@@ -83,9 +83,9 @@ export class HeadingManager {
8383
* @param slugger A _GithubSlugger_ instance
8484
* @param meta Document metadata gathered by `headingsMdPlugin`.
8585
*/
86-
constructor(slugger: GithubSlugger, meta: Partial<PageMetadata>) {
86+
constructor(slugger: GithubSlugger, meta: Metadata) {
8787
this.slugger = slugger;
88-
this._meta = { headings: meta.headings || [], ...meta };
88+
this._meta = meta;
8989
this._headings = [];
9090

9191
const maxHeadingDepth = meta.maxHeadingDepth ?? DEFAULT_NESTING_LIMIT;
@@ -102,7 +102,8 @@ export class HeadingManager {
102102
addHeading(
103103
title: string | object,
104104
nestingLevel?: number,
105-
additionalProps?: AdditionalProps
105+
additionalProps?: AdditionalProps,
106+
id?: string
106107
): Heading {
107108
// NOTE (barthap): workaround for complex titles containing both normal text and inline code
108109
// changing this needs also change in `headingsMdPlugin.js` to make metadata loading correctly
@@ -111,10 +112,10 @@ export class HeadingManager {
111112
const { hideInSidebar, sidebarTitle, sidebarDepth, sidebarType } = additionalProps ?? {};
112113
const levelOverride = sidebarDepth != null ? BASE_HEADING_LEVEL + sidebarDepth : undefined;
113114

114-
const slug = Utilities.generateSlug(this.slugger, title);
115+
const slug = id ?? Utilities.generateSlug(this.slugger, title);
115116
const realTitle = Utilities.toString(title);
116117
const meta = this.findMetaForTitle(realTitle);
117-
const level = levelOverride ?? nestingLevel ?? meta?.level ?? BASE_HEADING_LEVEL;
118+
const level = levelOverride ?? nestingLevel ?? meta?.depth ?? BASE_HEADING_LEVEL;
118119
const type = sidebarType || (this.isCode(title) ? HeadingType.InlineCode : HeadingType.Text);
119120

120121
const heading = {

docs/common/md-loader.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ module.exports = function (src) {
44
const { body, attributes } = fm(src);
55

66
return (
7-
`import withDocumentationElements from '~/components/page-higher-order/withDocumentationElements';
7+
`import DocumentationElements from '~/components/page-higher-order/DocumentationElements';
88
99
export const meta = ${JSON.stringify(attributes)}
1010
11-
export default withDocumentationElements(meta);
11+
export default DocumentationElements;
1212
1313
` + body
1414
);

docs/components/DocumentationSidebarRight.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { HeadingManager, HeadingType } from '~/common/headingManager';
88
import { HeadingsContext } from '~/components/page-higher-order/withHeadingManager';
99

1010
const prepareHeadingManager = () => {
11-
const headingManager = new HeadingManager(new GithubSlugger(), {});
11+
const headingManager = new HeadingManager(new GithubSlugger(), { headings: [] });
1212
headingManager.addHeading('Base level heading', undefined, {});
1313
headingManager.addHeading('Level 3 subheading', 3, {});
1414
headingManager.addHeading('Code heading depth 1', 0, {

docs/components/Permalink.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,12 @@ const Permalink: React.FC<EnhancedProps> = withHeadingManager(props => {
8888
let heading;
8989

9090
if (props.nestingLevel) {
91-
heading = props.headingManager.addHeading(children, props.nestingLevel, props.additionalProps);
91+
heading = props.headingManager.addHeading(
92+
children,
93+
props.nestingLevel,
94+
props.additionalProps,
95+
permalinkKey
96+
);
9297
}
9398

9499
if (!permalinkKey && heading?.slug) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { MDXProvider } from '@mdx-js/react';
2+
import GithubSlugger from 'github-slugger';
3+
import { useRouter } from 'next/router';
4+
import React, { PropsWithChildren } from 'react';
5+
6+
import { HeadingManager } from '~/common/headingManager';
7+
import * as components from '~/common/translate-markdown';
8+
import DocumentationPage from '~/components/DocumentationPage';
9+
import { HeadingsContext } from '~/components/page-higher-order/withHeadingManager';
10+
import { PageMetadata, RemarkHeading } from '~/types/common';
11+
12+
type DocumentationElementsProps = PropsWithChildren<{
13+
meta: PageMetadata;
14+
headings: RemarkHeading[];
15+
}>;
16+
17+
export default function DocumentationElements(props: DocumentationElementsProps) {
18+
const router = useRouter();
19+
const manager = new HeadingManager(new GithubSlugger(), {
20+
...props.meta,
21+
headings: props.headings,
22+
});
23+
24+
return (
25+
<HeadingsContext.Provider value={manager}>
26+
<DocumentationPage
27+
title={props.meta.title}
28+
url={router}
29+
asPath={router.asPath}
30+
sourceCodeUrl={props.meta.sourceCodeUrl}
31+
tocVisible={!props.meta.hideTOC}
32+
hideFromSearch={props.meta.hideFromSearch}>
33+
<MDXProvider components={components}>{props.children}</MDXProvider>
34+
</DocumentationPage>
35+
</HeadingsContext.Provider>
36+
);
37+
}

docs/components/page-higher-order/withDocumentationElements.tsx

-34
This file was deleted.

docs/components/plugins/APISection.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import APISection from './APISection';
88
import { HeadingManager } from '~/common/headingManager';
99

1010
const Wrapper: FC = ({ children }) => (
11-
<HeadingsContext.Provider value={new HeadingManager(new GithubSlugger(), {})}>
11+
<HeadingsContext.Provider value={new HeadingManager(new GithubSlugger(), { headings: [] })}>
1212
{children}
1313
</HeadingsContext.Provider>
1414
);

docs/components/plugins/AppConfigSchemaPropertiesTable.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ const testSchema: Record<string, Property> = {
100100
describe('AppConfigSchemaPropertiesTable', () => {
101101
test('correctly matches snapshot', () => {
102102
const { container } = render(
103-
<HeadingsContext.Provider value={new HeadingManager(new GithubSlugger(), {})}>
103+
<HeadingsContext.Provider value={new HeadingManager(new GithubSlugger(), { headings: [] })}>
104104
<AppConfigSchemaPropertiesTable schema={testSchema} />
105105
</HeadingsContext.Provider>
106106
);

docs/jest.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/** @type {import('@jest/types').Config.InitialOptions} */
12
module.exports = {
23
displayName: 'docs',
34
testEnvironment: 'jsdom',
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
const visit = require('unist-util-visit');
2+
3+
/**
4+
* @typedef {import('unist').Parent} Root - https://github.com/syntax-tree/mdast#root
5+
* @typedef {import('unist').Parent} Heading - https://github.com/syntax-tree/mdast#heading
6+
*/
7+
8+
/**
9+
* Find all headings within a MDX document, and export them as JS array.
10+
* This uses the MDAST's `heading` and tries to guess the children's content.
11+
* When the node has an ID, generated by `remark-slug`, the ID is added to the exported object.
12+
*
13+
* @param {object} options
14+
* @param {string} [options.exportName="headings"]
15+
*/
16+
module.exports = function remarkExportHeadings(options = {}) {
17+
const { exportName = 'headings' } = options;
18+
19+
/** @param {Root} tree */
20+
return tree => {
21+
const headings = [];
22+
23+
/** @param {Heading} node - */
24+
const visitor = node => {
25+
if (node.children.length > 0) {
26+
headings.push({
27+
id: node.data?.id,
28+
depth: node.depth,
29+
type: node.children.find(node => node.type !== 'text')?.type || 'text',
30+
title: node.children.map(child => child.value).join(' '),
31+
});
32+
}
33+
};
34+
35+
visit(tree, 'heading', visitor);
36+
tree.children.push({
37+
type: 'export',
38+
value: `export const ${exportName} = ${JSON.stringify(headings)};`,
39+
});
40+
};
41+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import u from 'unist-builder';
2+
3+
import exportHeadings from './remark-export-headings';
4+
5+
/**
6+
* See all MDAST types here https://github.com/syntax-tree/mdast#root.
7+
* All nodes are based on the Universal Syntax Tree (unist) system.
8+
*
9+
* @typedef {import('unist').Node} Node
10+
* @typedef {import('unist').Parent} Parent
11+
*/
12+
13+
describe('exports constant', () => {
14+
it('when no headers are found', () => {
15+
const { data } = transform(u('root', [u('text', 'lorem ipsum')]));
16+
expect(data).not.toBeNull();
17+
expect(data).toHaveLength(0);
18+
});
19+
20+
it('when single header is found', () => {
21+
const { data } = transform(u('root', [u('heading', [u('text', 'lorem ipsum')])]));
22+
expect(data).toHaveLength(1);
23+
});
24+
25+
it('when multiple headers are found', () => {
26+
const { data } = transform(
27+
u('root', [u('heading', [u('text', 'lorem ipsum')]), u('heading', [u('text', 'sit amet')])])
28+
);
29+
expect(data).toHaveLength(2);
30+
});
31+
});
32+
33+
describe('header object', () => {
34+
it('has title from text child', () => {
35+
const { data } = transform(u('root', [u('heading', [u('text', 'header title')])]));
36+
expect(data[0]).toHaveProperty('title', 'header title');
37+
});
38+
39+
it('has title from multiple text children', () => {
40+
const { data } = transform(
41+
u('root', [u('heading', [u('text', 'header'), u('text', 'title')])])
42+
);
43+
expect(data[0]).toHaveProperty('title', 'header title');
44+
});
45+
46+
it('has depth from heading', () => {
47+
const { data } = transform(u('root', [u('heading', { depth: 3 }, [u('text', 'title')])]));
48+
expect(data[0]).toHaveProperty('depth', 3);
49+
});
50+
51+
it('has id when defined as data', () => {
52+
const { data } = transform(
53+
u('root', [u('heading', { data: { id: 'title' } }, [u('text', 'title')])])
54+
);
55+
expect(data[0]).toHaveProperty('id', 'title');
56+
});
57+
58+
it('has text type from text children', () => {
59+
const { data } = transform(
60+
u('root', [u('heading', [u('text', 'hello there'), u('text', 'general kenobi')])])
61+
);
62+
expect(data[0]).toHaveProperty('type', 'text');
63+
});
64+
65+
it('has inlineCode type from mixed children', () => {
66+
const { data } = transform(
67+
u('root', [u('heading', [u('text', 'hello there'), u('inlineCode', 'general kenobi')])])
68+
);
69+
expect(data[0]).toHaveProperty('type', 'inlineCode');
70+
});
71+
});
72+
73+
/**
74+
* Helper function to run the MDAST transform, and find the added node.
75+
*
76+
* @param {Parent} tree
77+
* @param {object} [options]
78+
* @param {string} [options.exportName]
79+
*/
80+
function transform(tree, options = {}) {
81+
exportHeadings(options)(tree);
82+
83+
const value = `export const ${options.exportName || 'headings'} = `;
84+
const node = tree.children
85+
.reverse()
86+
.find(node => node.type === 'export' && node.value.startsWith(value));
87+
88+
const json = node ? node.value.replace(value, '').replace(/;$/, '') : null;
89+
const data = json ? JSON.parse(json) : null;
90+
91+
return { node, json, data };
92+
}

0 commit comments

Comments
 (0)