Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
19 changes: 18 additions & 1 deletion __tests__/lib/mdxish/magic-blocks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,21 @@ ${JSON.stringify(
expect((cell1.children[0] as Element).tagName).toBe('em');
});
});
});

describe('callout block', () => {
it('should restore callout block', () => {
const md = '[block:callout]{"type":"info","title":"Note","body":"This is important"}[/block]';

const ast = mdxish(md);
expect(ast.children).toHaveLength(1);
expect(ast.children[0].type).toBe('element');

const calloutElement = ast.children[0] as Element;
expect(calloutElement.tagName).toBe('Callout');
expect(calloutElement.properties.type).toBe('info');
expect(calloutElement.properties.theme).toBe('info');
expect(calloutElement.properties.icon).toBe('📘');
expect(calloutElement.children).toHaveLength(2);
});
});
});
90 changes: 72 additions & 18 deletions processor/transform/mdxish/mdxish-magic-blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
*/
import type { BlockHit } from '../../../lib/utils/extractMagicBlocks';
import type { Code, Parent, Root as MdastRoot, RootContent } from 'mdast';
import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
import type { Plugin } from 'unified';

import remarkGfm from 'remark-gfm';
import remarkParse from 'remark-parse';
import { unified } from 'unified';
import { visit } from 'unist-util-visit';
import { SKIP, visit } from 'unist-util-visit';

import { toAttributes } from '../../utils';

/**
* Matches legacy magic block syntax: [block:TYPE]...JSON...[/block]
Expand Down Expand Up @@ -260,19 +263,36 @@ function parseMagicBlock(raw: string, options: ParseMagicBlockOptions = {}): Mda

if (!(calloutJson.title || calloutJson.body)) return [];

return [
wrapPinnedBlocks(
{
children: [...textToBlock(calloutJson.title || ''), ...textToBlock(calloutJson.body || '')],
data: {
hName: 'rdme-callout',
hProperties: { icon, theme: theme || 'default', title: calloutJson.title, value: calloutJson.body },
},
type: 'rdme-callout',
},
json,
),
];
const titleBlocks = textToBlock(calloutJson.title || '');
const bodyBlocks = textToBlock(calloutJson.body || '');

const children: MdastNode[] = [];
if (titleBlocks.length > 0 && titleBlocks[0].type === 'paragraph') {
const firstTitle = titleBlocks[0] as { children?: MdastNode[] };
const heading = {
type: 'heading',
depth: 3,
children: (firstTitle.children || []) as unknown[],
};
children.push(heading as unknown as MdastNode);
children.push(...titleBlocks.slice(1), ...bodyBlocks);
} else {
children.push(...titleBlocks, ...bodyBlocks);
}

// Create mdxJsxFlowElement directly for mdxish
const calloutElement: MdxJsxFlowElement = {
type: 'mdxJsxFlowElement',
name: 'Callout',
attributes: toAttributes({ icon, theme: theme || 'default', type: theme || 'default' }, [
'icon',
'theme',
'type',
]),
children: children as MdxJsxFlowElement['children'],
};

return [wrapPinnedBlocks(calloutElement as unknown as MdastNode, json)];
}

// Parameters: renders as a table (used for API parameters, etc.)
Expand Down Expand Up @@ -372,6 +392,13 @@ function parseMagicBlock(raw: string, options: ParseMagicBlockOptions = {}): Mda
}
}

/**
* Check if a child node is a flow element that needs unwrapping (mdxJsxFlowElement, etc.)
*/
const needsUnwrapping = (child: RootContent): boolean => {
return child.type === 'mdxJsxFlowElement';
};

/**
* Unified plugin that restores magic blocks from placeholder tokens.
*
Expand All @@ -388,15 +415,42 @@ const magicBlockRestorer: Plugin<[{ blocks: BlockHit[] }], MdastRoot> =
const magicBlockKeys = new Map(blocks.map(({ key, raw }) => [key, raw] as const));

// Find inlineCode nodes that match our placeholder tokens
const modifications: { children: RootContent[]; index: number; parent: Parent }[] = [];

visit(tree, 'inlineCode', (node: Code, index: number, parent: Parent) => {
if (!parent || index == null) return;
if (!parent || index == null) return undefined;
const raw = magicBlockKeys.get(node.value);
if (!raw) return;
if (!raw) return undefined;

// Parse the original magic block and replace the placeholder with the result
const children = parseMagicBlock(raw) as unknown as RootContent[];
if (!children.length) return;
if (!children.length) return undefined;

if (children[0] && needsUnwrapping(children[0]) && parent.type === 'paragraph') {
// Find paragraph's parent and unwrap
let paragraphParent: Parent | undefined;
visit(tree, 'paragraph', (p, pIndex, pParent) => {
if (p === parent && pParent && 'children' in pParent) {
paragraphParent = pParent as Parent;
return false;
}
return undefined;
});

if (paragraphParent) {
const paragraphIndex = paragraphParent.children.indexOf(parent as RootContent);
if (paragraphIndex !== -1) {
modifications.push({ children, index: paragraphIndex, parent: paragraphParent });
}
}
return SKIP;
}

parent.children.splice(index, 1, ...children);
return [SKIP, index + children.length];
});

// Apply modifications in reverse order to avoid index shifting
modifications.reverse().forEach(({ children, index, parent }) => {
parent.children.splice(index, 1, ...children);
});
};
Expand Down