|
1 | 1 | import { DocConfig } from "docgen/types";
|
2 |
| -import MarkdownJSX from "markdown-to-jsx"; |
3 |
| -import React from "react"; |
| 2 | +import { MarkdownToJSX, compiler } from "markdown-to-jsx"; |
| 3 | +import React, { |
| 4 | + Children, |
| 5 | + isValidElement, |
| 6 | + ReactElement, |
| 7 | + ReactNode, |
| 8 | +} from "react"; |
| 9 | +import { CSS, PageBreak, Tailwind } from ".."; |
4 | 10 |
|
5 |
| -export const Markdown = MarkdownJSX; |
| 11 | +interface TocRendererProps { |
| 12 | + heading: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; |
| 13 | + level: number; |
| 14 | + children: ReactNode; |
| 15 | + id: string; |
| 16 | +} |
| 17 | + |
| 18 | +interface MarkdownProps { |
| 19 | + children: string; |
| 20 | + tocRenderer?: (props: TocRendererProps) => ReactNode; |
| 21 | + options?: MarkdownToJSX.Options; |
| 22 | +} |
| 23 | + |
| 24 | +export const Markdown = (props: MarkdownProps) => { |
| 25 | + const content = compiler(props.children, props.options); |
| 26 | + |
| 27 | + let headers: TocRendererProps[] = []; |
| 28 | + |
| 29 | + const isReactElement = (child: ReactNode): child is ReactElement<any> => { |
| 30 | + return typeof child === "object" && child !== null && "type" in child; |
| 31 | + }; |
| 32 | + |
| 33 | + const detectHeader = (child: ReactNode) => { |
| 34 | + if (!child) return; |
| 35 | + |
| 36 | + if ( |
| 37 | + isReactElement(child) && |
| 38 | + typeof child.type === "string" && |
| 39 | + ["h1", "h2", "h3", "h4", "h5", "h6"].includes(child.type) |
| 40 | + ) { |
| 41 | + headers.push({ |
| 42 | + heading: child.type, |
| 43 | + level: parseInt(child.type[1]), |
| 44 | + children: child.props.children, |
| 45 | + id: child.props.id, |
| 46 | + } as TocRendererProps); |
| 47 | + } |
| 48 | + |
| 49 | + if (isValidElement(child)) { |
| 50 | + if ( |
| 51 | + typeof child.type === "function" && |
| 52 | + child.type.prototype && |
| 53 | + child.type.prototype.isReactComponent |
| 54 | + ) { |
| 55 | + // @ts-ignore |
| 56 | + const instance = new child.type(child.props); // Instantiate the class component |
| 57 | + const result = instance.render(); // Call its render method |
| 58 | + detectHeader(result); |
| 59 | + } else if (typeof child.type === "function") { |
| 60 | + // @ts-ignore |
| 61 | + const result = child.type(child.props); // call the component |
| 62 | + detectHeader(result); |
| 63 | + } else if (child.props && child.props.children) { |
| 64 | + Children.forEach(child.props.children, detectHeader); |
| 65 | + } |
| 66 | + } |
| 67 | + }; |
| 68 | + |
| 69 | + const tocRenderer = props.tocRenderer; |
| 70 | + |
| 71 | + if (tocRenderer) detectHeader(content); |
| 72 | + |
| 73 | + const Toc = !!tocRenderer ? ( |
| 74 | + <>{headers.map((header) => tocRenderer(header))}</> |
| 75 | + ) : null; |
| 76 | + |
| 77 | + return compiler( |
| 78 | + props.children, |
| 79 | + Object.assign({}, props.options, { |
| 80 | + overrides: { |
| 81 | + Toc: !!tocRenderer |
| 82 | + ? { |
| 83 | + component: () => Toc, |
| 84 | + } |
| 85 | + : undefined, |
| 86 | + ...props.options?.overrides, |
| 87 | + }, |
| 88 | + }) |
| 89 | + ); |
| 90 | +}; |
6 | 91 |
|
7 | 92 | export const __docConfig: DocConfig = {
|
8 | 93 | description: `Render Markdown inside your templates. Provides a simple wrapper around [\`markdown-to-jsx\`](https://github.com/quantizor/markdown-to-jsx).
|
@@ -63,6 +148,64 @@ This agreement is signed with <CustomerName />.
|
63 | 148 | <KPI>20/month</KPI>`}</Markdown>
|
64 | 149 | ),
|
65 | 150 | },
|
| 151 | + tableOfContents: { |
| 152 | + name: "Table of Contents", |
| 153 | + description: `You can use the \`tocRenderer\` prop to render a table of contents from your Markdown content. The headers will be automatically detected and rendered in the order they appear. You need to place the \`<Toc />\` component in your Markdown content to render the table of contents. |
| 154 | +
|
| 155 | +You can also use the \`id\` attribute in your headers to link to them directly.`, |
| 156 | + template: ( |
| 157 | + <Tailwind |
| 158 | + config={{ |
| 159 | + corePlugins: { |
| 160 | + preflight: false, |
| 161 | + }, |
| 162 | + }} |
| 163 | + > |
| 164 | + <CSS>{`a.-toc-link:after { |
| 165 | + content: target-counter(attr(href), page); |
| 166 | + float: right; |
| 167 | + }`}</CSS> |
| 168 | + <Markdown |
| 169 | + options={{ |
| 170 | + overrides: { |
| 171 | + PageBreak: { |
| 172 | + component: PageBreak, // import { PageBreak } from "@fileforge/react-print"; |
| 173 | + }, |
| 174 | + }, |
| 175 | + }} |
| 176 | + tocRenderer={({ level, children, id }) => ( |
| 177 | + <a |
| 178 | + className="block py-2 border-b -toc-link" |
| 179 | + style={{ |
| 180 | + paddingLeft: `${(level - 1) * 1}rem`, |
| 181 | + }} |
| 182 | + href={`#${id}`} |
| 183 | + > |
| 184 | + {children} |
| 185 | + </a> |
| 186 | + )} |
| 187 | + >{`# Table of Contents |
| 188 | +
|
| 189 | +<Toc /> |
| 190 | +
|
| 191 | +<PageBreak /> |
| 192 | +
|
| 193 | +# This is a level 1 header |
| 194 | +
|
| 195 | +## This is a level 2 header |
| 196 | +
|
| 197 | +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. |
| 198 | +
|
| 199 | +## This is another level 2 header |
| 200 | +
|
| 201 | +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. |
| 202 | +
|
| 203 | +# This is a level 1 header, bis |
| 204 | +
|
| 205 | +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi.`}</Markdown> |
| 206 | + </Tailwind> |
| 207 | + ), |
| 208 | + }, |
66 | 209 | },
|
67 | 210 | },
|
68 | 211 | },
|
|
0 commit comments