diff --git a/__tests__/plugins/toc.test.tsx b/__tests__/plugins/toc.test.tsx
index a6367bd29..1b044abe6 100644
--- a/__tests__/plugins/toc.test.tsx
+++ b/__tests__/plugins/toc.test.tsx
@@ -175,4 +175,40 @@ export const toc = [
"
`);
});
+
+ it('preserves nesting when component names differ from exported elements', () => {
+ const md = `
+# Title
+
+## SubHeading
+
+
+ First
+
+`;
+
+ const components = {
+ Comp: 'export const Comp = ({ children }) => { return children; }',
+ };
+
+ const compModule = run(compile(components.Comp));
+ const { Toc } = run(compile(md, { components }), {
+ components: {
+ // 👇🏼 this is what we're guarding against
+ CompDoesNotMatchExportedModule: compModule,
+ },
+ });
+
+ const html = renderToString();
+ expect(html).toMatchInlineSnapshot(`
+ ""
+ `);
+ });
});
diff --git a/processor/plugin/toc.ts b/processor/plugin/toc.ts
index b6d947d58..134a51dc4 100644
--- a/processor/plugin/toc.ts
+++ b/processor/plugin/toc.ts
@@ -58,14 +58,31 @@ export const rehypeToc = ({ components = {} }: Options): Transformer
};
const MAX_DEPTH = 2;
-const getDepth = (el: HastHeading) => parseInt(el.tagName?.match(/^h(\d)/)[1], 10);
+
+/**
+ * Get the depth of a heading element based on its tag name.
+ *
+ * ⚠️ Be extra defensive to non-heading elements somehow making it here. This
+ * should not happen, but if it does, we avoid breaking TOC depth calculations
+ * by returning `Infinity`, thus removing them from depth considerations.
+ * @link https://linear.app/readme-io/issue/CX-2543/tabapay-toc-does-not-respect-headers-nesting
+ *
+ * @example
+ * getDepth({ tagName: 'h1' }) // 1
+ * getDepth({ tagName: 'h2' }) // 2
+ */
+const getDepth = (el: HastHeading) => {
+ if (!el.tagName) return Infinity;
+ return parseInt(el.tagName?.match(/^h(\d)/)[1], 10);
+};
/*
* `tocToHast` consumes the list generated by `rehypeToc` and produces a hast
* of nested lists to be rendered as a table of contents.
*/
const tocToHast = (headings: HastHeading[] = []): TocList => {
- const min = Math.min(...headings.map(getDepth));
+ const headingDepths = headings.map(getDepth);
+ const min = Math.min(...headingDepths);
const ast = h('ul') as TocList;
const stack: TocList[] = [ast];
@@ -105,7 +122,10 @@ export const tocHastToMdx = (toc: IndexableElements[] | undefined, components: R
if (typeof toc === 'undefined') return '';
const injected = toc.flatMap(node => {
- return node.type === 'mdxJsxFlowElement' && node.name in components ? components[node.name] || [] : node;
+ if (node.type === 'mdxJsxFlowElement') {
+ return components[node.name] || [];
+ }
+ return node;
});
const tocHast = tocToHast(injected as HastHeading[]);