Skip to content

Commit

Permalink
fix: nesting children (#1060)
Browse files Browse the repository at this point in the history
| [![PR App][icn]][demo] | Ref RM-12072 |
| :--------------------: | :----------: |

## 🧰 Changes

Fixes a crash when rendering exported components.

The error was caused because we were passing in an map of custom block
tags (not exports) to `tocToMdx`. The `tocToMdx` function would parse
the toc for tags, and see if they had an associated `toc` export. By for
exported components, that (`component[node.name]`) would be `undefined`,
and we would crash. This refactors it to be slightly clearer and less
error prone.

## ✅ TODO

**How do we handle generating a Table of Contents for Custom
Components??**

We currently support parsing a table of contents from the default
export, because that's how reusable content works. This is probably not
the best way to do it with regards to custom components. The default, or
any of the exports, could be JSX or markdown or any kinda of ESM object.

## 🧬 QA & Testing

- [Broken on production][prod].
- [Working in this PR app][demo].

[demo]: https://markdown-pr-PR_NUMBER.herokuapp.com
[prod]: https://SUBDOMAIN.readme.io
[icn]:
https://user-images.githubusercontent.com/886627/160426047-1bee9488-305a-4145-bb2b-09d8b757d38a.svg
  • Loading branch information
kellyjosephprice authored Feb 25, 2025
1 parent 9a8743e commit 2824e1f
Show file tree
Hide file tree
Showing 8 changed files with 52 additions and 9 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions __tests__/browser/markdown.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe('visual regression tests', () => {
const docs = [
'callouts',
'calloutTests',
'childTests',
'codeBlocks',
// skipping this because they sporadically failure with network timing
// issues
Expand Down
26 changes: 22 additions & 4 deletions __tests__/custom-components/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import type { RMDXModule } from '../../types';
import { render, screen } from '@testing-library/react';
import React from 'react';

import { execute } from '../helpers';
import { compile, run } from '../../lib';
import { RMDXModule } from '../../types';
import { execute } from '../helpers';

describe('Custom Components', () => {
let Example;
let Multiple;
let Nesting;

beforeEach(async () => {
Example = await execute('It works!', {}, {}, { getDefault: false });
Expand All @@ -22,6 +22,16 @@ export const Second = () => <div>Second</div>;
{},
{ getDefault: false },
);
Nesting = await execute(
`
export const WithChildren = ({ children }) => <div>{children}</div>;
<div>{props.children}</div>
`,
{},
{},
{ getDefault: false },
);
});

it('renders custom components', async () => {
Expand All @@ -47,9 +57,17 @@ export const Second = () => <div>Second</div>;
expect(screen.getByText('Second')).toBeVisible();
});

it('renders a nested exported custom component', async () => {
const doc = '<Nesting><WithChildren>Hello, Test User!</WithChildren></Nesting>';
const Page = (await execute(doc, undefined, { components: { Nesting } })) as RMDXModule['default'];
render(<Page />);

expect(screen.getByText('Hello, Test User!')).toBeVisible();
});

it('renders the default export of a custom component and passes through props', async () => {
const Test = (await run(await compile(`{props.attr}`))) as RMDXModule;
const doc = `<Test attr="Hello" />`;
const Test = (await run(await compile('{props.attr}'))) as RMDXModule;
const doc = '<Test attr="Hello" />';
const Page = await run(await compile(doc), { components: { Test } });
render(<Page.default />);

Expand Down
4 changes: 4 additions & 0 deletions __tests__/fixtures/child-tests.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<Steps>
<Step>Step One</Step>
<Step>Step Two</Step>
</Steps>
15 changes: 15 additions & 0 deletions example/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ export const StyledComponent = () => {
</div>;
}
`,
Steps: `
export const Step = ({ children }) => {
return (
<div className="flex items-center h-full w-full">
<div className="bg-gray-800 rounded-md p-6 m-6">
{children}
</div>
</div>
);
};
<div className="bg-gray-500 rounded-md p-3 m-3">
{props.children}
</div>
`,
};

export default components;
2 changes: 2 additions & 0 deletions example/docs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import calloutTests from '../__tests__/fixtures/callout-tests.md';
import childTests from '../__tests__/fixtures/child-tests.mdx';
import codeBlockTests from '../__tests__/fixtures/code-block-tests.md';
import exportTests from '../__tests__/fixtures/export-tests.mdx';
import imageTests from '../__tests__/fixtures/image-tests.mdx';
Expand All @@ -25,6 +26,7 @@ const lowerCase = (str: string) =>
const fixtures = Object.entries({
calloutTests,
callouts,
childTests,
codeBlockTests,
codeBlocks,
embeds,
Expand Down
7 changes: 5 additions & 2 deletions lib/run.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import * as runtime from 'react/jsx-runtime';

import * as Components from '../components';
import Contexts from '../contexts';
import { tocToMdx } from '../processor/plugin/toc';
import { tocHastToMdx } from '../processor/plugin/toc';
import User from '../utils/user';

import compile from './compile';
Expand Down Expand Up @@ -54,10 +54,13 @@ const makeUseMDXComponents = (more: ReturnType<UseMdxComponents> = {}): UseMdxCo
const run = async (string: string, _opts: RunOpts = {}) => {
const { Fragment } = runtime;
const { components = {}, terms, variables, baseUrl, imports = {}, ...opts } = _opts;

const tocsByTag: Record<string, RMDXModule['toc']> = {};
const exportedComponents = Object.entries(components).reduce((memo, [tag, mod]) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { default: Content, toc, Toc, stylesheets, ...rest } = mod;
memo[tag] = Content;
tocsByTag[tag] = toc;

if (rest) {
Object.entries(rest).forEach(([subTag, component]) => {
Expand All @@ -83,7 +86,7 @@ const run = async (string: string, _opts: RunOpts = {}) => {
const { Toc: _Toc, toc, default: Content, stylesheet, ...exports } = await exec(string);

let Toc: React.FC | undefined;
const tocMdx = tocToMdx(toc, components);
const tocMdx = tocHastToMdx(toc, tocsByTag);
if (tocMdx) {
const compiledToc = await compile(tocMdx);
const tocModule = await exec(compiledToc, { useMDXComponents: () => ({ p: Fragment }) });
Expand Down
6 changes: 3 additions & 3 deletions processor/plugin/toc.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CustomComponents, HastHeading, IndexableElements, TocList, TocListItem } from '../../types';
import type { CustomComponents, HastHeading, IndexableElements, RMDXModule, TocList, TocListItem } from '../../types';
import type { Root } from 'hast';
import type { MdxjsEsm } from 'mdast-util-mdxjs-esm';
import type { Transformer } from 'unified';
Expand Down Expand Up @@ -89,11 +89,11 @@ const tocToHast = (headings: HastHeading[] = []): TocList => {
return ast;
};

export const tocToMdx = (toc: IndexableElements[], components: CustomComponents) => {
export const tocHastToMdx = (toc: IndexableElements[], components: Record<string, RMDXModule['toc']>) => {
const tree: Root = { type: 'root', children: toc };

visit(tree, 'mdxJsxFlowElement', (node, index, parent) => {
const subToc = components[node.name].toc || [];
const subToc = components[node.name] || [];
parent.children.splice(index, 1, ...subToc);
});

Expand Down

0 comments on commit 2824e1f

Please # to comment.