Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
refactor: redo toc
  • Loading branch information
kellyjosephprice committed Jun 6, 2024
commit b01b574a0185fc7bacde444c952cd2291fb953db
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { compile } from '../../index';
import { h } from 'hastscript';

describe('rehype-toc transformer', () => {
describe('toc transformer', () => {
it('parses out a toc with max depth of 2', () => {
const md = `
# Title
Expand Down
1 change: 1 addition & 0 deletions components/TableOfContents/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';

function TableOfContents({ children }: React.PropsWithChildren) {
console.log('wat');
return (
<nav>
<ul className="toc-list">
Expand Down
11 changes: 8 additions & 3 deletions docs/table-of-contents-tests.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
---
title: "Table Of Contents Tests"
title: 'Table Of Contents Tests'
category: 5fdf9fc9c2a7ef443e937315
hidden: true
---
# Variables (<<user>>)

# Glossary Items (<<glossary:demo>>)
# Variables <Variable name="email" />

# Glossary Items <Glossary>demo</Glossary>

## Custom Components

<Demo />
30 changes: 18 additions & 12 deletions example/Doc.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
import React, { FunctionComponent, useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useParams, useSearchParams } from 'react-router-dom';
import * as mdx from '../index';
import docs from './docs';
import RenderError from './RenderError';
import { MDXContent, MDXModule } from 'mdx/types';
import { MDXContent } from 'mdx/types';

const mdxComponents = {
const components = {
Demo: `
## This is a Demo Component!

> 📘 It can render JSX components!
`,
};

const compiledComponents = {};
const components = {};
Object.keys(mdxComponents).forEach(async comp => {
compiledComponents[comp] = mdx.compile(comp);
components[comp] = (await mdx.run(compiledComponents[comp])).default;
Object.entries(components).forEach(([tag, body]) => {
components[tag] = mdx.compile(body);
});

const variables = {
user: {
email: '[email protected]',
},
defaults: [],
};

const terms = [
{
term: 'demo',
Expand All @@ -45,9 +49,9 @@ const Doc = () => {
const [name, doc] =
fixture === 'edited' ? [fixture, searchParams.get('edit') || ''] : [docs[fixture].name, docs[fixture].doc];

const [{ default: Content, toc: Toc }, setContent] = useState<{ default: MDXContent; toc?: MDXContent }>({
const [{ default: Content, Toc }, setContent] = useState<{ default: MDXContent; Toc?: MDXContent }>({
default: null,
toc: null,
Toc: null,
});
const [error, setError] = useState<string>(null);

Expand All @@ -59,8 +63,8 @@ const Doc = () => {
};

try {
const code = mdx.compile(doc, opts);
const content = await mdx.run(code, { components, terms });
const code = mdx.compile(doc, { ...opts, components });
const content = await mdx.run(code, { components, terms, variables });

setError(() => null);
setContent(() => content);
Expand Down Expand Up @@ -91,5 +95,7 @@ const Doc = () => {
</div>
);
};
/*
*/

export default Doc;
56 changes: 19 additions & 37 deletions lib/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,53 +3,35 @@ import remarkFrontmatter from 'remark-frontmatter';
import remarkGfm from 'remark-gfm';
import rehypeSlug from 'rehype-slug';

import transformers, { rehypeToc } from '../processor/transform';
import { VFileWithToc } from '../types';
import transformers from '../processor/transform';
import { rehypeToc } from '../processor/plugin/toc';
import MdxSyntaxError from '../errors/mdx-syntax-error';
import mdx from './mdx';

export type CompileOpts = CompileOptions & {
components?: Record<string, VFileWithToc>;
lazyImages?: boolean;
safeMode?: boolean;
components?: Record<string, string>;
};

const remarkPlugins = [remarkFrontmatter, remarkGfm, ...transformers];

const compile = (text: string, opts: CompileOpts = {}) => {
const { components } = opts;

const exec = (string: string): VFileWithToc => {
try {
return compileSync(string, {
outputFormat: 'function-body',
providerImportSource: '#',
remarkPlugins,
rehypePlugins: [rehypeSlug, [rehypeToc, { components }]],
...opts,
});
} catch (error) {
throw error.line ? new MdxSyntaxError(error, text) : error;
}
};

const vfile = exec(text);
if (vfile.data.toc.ast) {
const toc = mdx(vfile.data.toc.ast, { hast: true });

if (toc) {
vfile.data.toc.vfile = exec(toc);
}
} else {
delete vfile.data.toc;
const compile = (text: string, { components, ...opts }: CompileOpts = {}) => {
try {
const vfile = compileSync(text, {
outputFormat: 'function-body',
providerImportSource: '#',
remarkPlugins,
rehypePlugins: [rehypeSlug, [rehypeToc, { components }]],
...opts,
});

return String(vfile).replace(
/await import\(_resolveDynamicMdxSpecifier\(('react'|"react")\)\)/,
'arguments[0].imports.React',
);
} catch (error) {
throw error.line ? new MdxSyntaxError(error, text) : error;
}

vfile.value = String(vfile).replace(
/await import\(_resolveDynamicMdxSpecifier\(('react'|"react")\)\)/,
'arguments[0].imports.React',
);

return vfile;
};

export default compile;
67 changes: 48 additions & 19 deletions lib/run.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,47 @@
import React from 'react';
import { VFile } from 'vfile';
import * as runtime from 'react/jsx-runtime';

import { RunOptions, run as mdxRun } from '@mdx-js/mdx';
import { RunOptions, UseMdxComponents, run as mdxRun } from '@mdx-js/mdx';
import Variable from '@readme/variable';

import * as Components from '../components';
import Contexts from '../contexts';
import { VFileWithToc } from '../types';
import { GlossaryTerm } from '../contexts/GlossaryTerms';
import { Depth } from '../components/Heading';
import { visit } from 'unist-util-visit';
import { tocToHast } from '../processor/plugin/toc';
import compile from './compile';
import mdx from './mdx';
import { MDXModule } from 'mdx/types';
import { Root } from 'hast';
import { HastHeading, IndexableElements } from 'types';

interface Variables {
user: Record<string, string>;
defaults: { name: string; default: string }[];
}

type CompiledComponents = Record<string, string>;

export type RunOpts = Omit<RunOptions, 'Fragment'> & {
components?: ComponentOpts;
components?: CompiledComponents;
imports?: Record<string, unknown>;
baseUrl?: string;
terms?: GlossaryTerm[];
variables?: Variables;
};

type ComponentOpts = Record<string, (props: any) => React.ReactNode>;
interface RMDXModule extends MDXModule {
toc: IndexableElements[];
}

const makeUseMDXComponents = (more: RunOpts['components']): (() => ComponentOpts) => {
const makeUseMDXComponents = (more: ReturnType<UseMdxComponents> = {}): UseMdxComponents => {
const headings = Array.from({ length: 6 }).reduce((map, _, index) => {
map[`h${index + 1}`] = Components.Heading((index + 1) as Depth);
return map;
}, {});

const components = {
...more,
...Components,
Variable,
code: Components.Code,
Expand All @@ -42,40 +50,61 @@ const makeUseMDXComponents = (more: RunOpts['components']): (() => ComponentOpts
embed: Components.Embed,
img: Components.Image,
table: Components.Table,
'table-of-contents': Components.TableOfContents,
// @ts-expect-error
...headings,
...more,
};

return () => components;
};

const run = async (stringOrFile: string | VFileWithToc, _opts: RunOpts = {}) => {
const run = async (string: string, _opts: RunOpts = {}) => {
const { Fragment } = runtime as any;
const { components, terms, variables, baseUrl, ...opts } = _opts;
const vfile = new VFile(stringOrFile) as VFileWithToc;
const { components = {}, terms, variables, baseUrl, ...opts } = _opts;

const exec = (text: string, __opts: RunOpts = {}) => {
const { useMDXComponents } = __opts;

const exec = (file: VFile | string, toc = false) =>
mdxRun(file, {
return mdxRun(text, {
...runtime,
Fragment,
baseUrl: import.meta.url,
imports: { React },
useMDXComponents: makeUseMDXComponents({ ...components, ...(toc && { p: Fragment }) }),
...__opts,
useMDXComponents: useMDXComponents ?? makeUseMDXComponents(),
...opts,
});
}) as Promise<RMDXModule>;
};

const promises = Object.entries(components).map(async ([tag, body]) => [tag, await exec(body)] as const);

const CustomComponents: Record<string, RMDXModule> = {};
(await Promise.all(promises)).forEach(([tag, node]) => {
CustomComponents[tag] = node;
});

const { toc, default: Content } = await exec(string, {
useMDXComponents: makeUseMDXComponents(
Object.fromEntries(Object.entries(CustomComponents).map(([tag, module]) => [tag, module.default])),
),
});

const tree: Root = { type: 'root', children: toc };
visit(tree, 'mdxJsxFlowElement', (node, index, parent) => {
parent.children.splice(index, 1, ...CustomComponents[node.name].toc);
});

const file = await exec(vfile);
const Content = file.default;
const { default: Toc } = 'toc' in vfile.data ? await exec(vfile.data.toc.vfile, true) : { default: null };
const tocHast = tocToHast(tree.children as HastHeading[]);
const tocMdx = mdx(tocHast, { hast: true });
const { default: Toc } = await exec(compile(tocMdx), { useMDXComponents: makeUseMDXComponents({ p: Fragment }) });

return {
default: () => (
<Contexts terms={terms} variables={variables} baseUrl={baseUrl}>
<Content />
</Contexts>
),
toc: () =>
Toc: () =>
Toc && (
<Components.TableOfContents>
<Toc />
Expand Down
14 changes: 14 additions & 0 deletions lib/variable-proxy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import { Variable } from '@readme/variable';

const VariableProxy = () =>
new Proxy(
{},
{
get(_, prop) {
return <Variable name={prop} />;
},
},
);

export default VariableProxy;
Loading