diff --git a/__tests__/plugins/toc.test.tsx b/__tests__/plugins/toc.test.tsx
index a649ba7bf..19ee62f44 100644
--- a/__tests__/plugins/toc.test.tsx
+++ b/__tests__/plugins/toc.test.tsx
@@ -115,4 +115,28 @@ export const toc = [
expect(screen.findByText('Title')).toBeDefined();
expect(screen.queryByText('Callout')).toBeNull();
});
+
+ it('includes headings from nested component tocs', () => {
+ const md = `
+ # Title
+
+
+ `;
+
+ const components = {
+ ParentInfo: '## Parent Heading',
+ };
+
+ const parentModule = run(compile(components.ParentInfo));
+
+ const executed = {
+ ParentInfo: parentModule,
+ };
+
+ const { Toc } = run(compile(md, { components }), { components: executed });
+
+ render();
+
+ expect(screen.findByText('Parent Heading')).toBeDefined();
+ });
});
diff --git a/processor/plugin/toc.ts b/processor/plugin/toc.ts
index 3eb85d0b4..1e413c15f 100644
--- a/processor/plugin/toc.ts
+++ b/processor/plugin/toc.ts
@@ -5,7 +5,7 @@ import type { Transformer } from 'unified';
import { valueToEstree } from 'estree-util-value-to-estree';
import { h } from 'hastscript';
-import { visit } from 'unist-util-visit';
+import { visit, SKIP } from 'unist-util-visit';
import { mdx, plain } from '../../lib';
import { hasNamedExport } from '../utils';
@@ -14,15 +14,75 @@ interface Options {
components?: CustomComponents;
}
+interface CalloutCandidateNode {
+ data?: unknown;
+ name?: unknown;
+ properties?: unknown;
+ tagName?: unknown;
+ type?: unknown;
+}
+
+const isCalloutNode = (node: unknown): boolean => {
+ if (!node || typeof node !== 'object') return false;
+ const { type, name, tagName, data, properties } = node as CalloutCandidateNode;
+
+ if (type === 'mdxJsxFlowElement' && name === 'Callout') {
+ return true;
+ }
+
+ if (type !== 'element') return false;
+
+ if (tagName === 'Callout') return true;
+
+ if (typeof data === 'object' && data && 'hName' in (data as Record)) {
+ const { hName } = data as { hName?: unknown };
+ if (hName === 'Callout') return true;
+ }
+
+ if (tagName !== 'blockquote') return false;
+
+ if (!properties || typeof properties !== 'object') return false;
+
+ const { className } = properties as { className?: unknown };
+ if (!className) return false;
+
+ if (Array.isArray(className)) {
+ return className.some(cls => typeof cls === 'string' && cls.startsWith('callout'));
+ }
+
+ if (typeof className === 'string') {
+ return className.includes('callout');
+ }
+
+ return false;
+};
+
export const rehypeToc = ({ components = {} }: Options): Transformer => {
return (tree: Root): void => {
if (hasNamedExport(tree, 'toc')) return;
- const headings = tree.children.filter(
- child =>
- (child.type === 'element' && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(child.tagName)) ||
- (child.type === 'mdxJsxFlowElement' && child.name in components),
- ) as IndexableElements[];
+ const headings: IndexableElements[] = [];
+
+ visit(tree, (node, _index, parent) => {
+ if (isCalloutNode(node)) {
+ return SKIP;
+ }
+
+ const insideCallout = parent ? isCalloutNode(parent) : false;
+ if (insideCallout) {
+ return undefined;
+ }
+
+ if (node.type === 'element' && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName)) {
+ headings.push(node as HastHeading);
+ }
+
+ if (node.type === 'mdxJsxFlowElement' && node.name && node.name in components) {
+ headings.push(node as IndexableElements);
+ }
+
+ return undefined;
+ });
tree.children.unshift({
type: 'mdxjsEsm',