Skip to content
Open
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
20 changes: 19 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,22 @@ ${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);
console.log(JSON.stringify(ast, null, 2));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
console.log(JSON.stringify(ast, null, 2));

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);
});
});
});
78 changes: 64 additions & 14 deletions processor/transform/mdxish/mdxish-magic-blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
*/
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 { toAttributes } from '../../utils';

/**
* Matches legacy magic block syntax: [block:TYPE]...JSON...[/block]
* Group 1: block type (e.g., "image", "code", "callout")
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 @@ -388,15 +408,45 @@ 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;
const raw = magicBlockKeys.get(node.value);
if (!raw) return;

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

// Check if first child is a flow element that needs unwrapping (mdxJsxFlowElement, etc.)
const needsUnwrapping = (child: RootContent): boolean => {
return child.type === 'mdxJsxFlowElement';
};
Comment on lines +421 to +424
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could move out of the top visitor function so it is only created once.


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 });
}
}
} else {
parent.children.splice(index, 1, ...children);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we are mutating parent.children do we need to return an index of the next node to visit here?

}
});

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