diff --git a/README.md b/README.md index 30dc382..23a44d4 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,12 @@ Transform GitHub-style markdown alerts into HTML using the [unified][unified] ec ## Features - 🎯 **GitHub compatibility** - Renders `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, and `[!CAUTION]` alerts +- 🔄 **Dual rendering modes** - Smart auto-detection for HTML and component-based pipelines (react-markdown, MDX) - 🛡️ **100% test coverage** - Comprehensive test suite - 🔧 **Maximum extensibility** - Configure HTML elements, class names, and custom icons per alert type - 🎨 **Unstyled by default** - No opinionated CSS, works with any design system - 📦 **TypeScript support** - Batteries included with typed HTML tags and more -- 🔧 **[Unified][unified] ecosystem** - Works with remark, rehype and can be easily used with [react-markdown][react-markdown] +- 🔧 **[Unified][unified] ecosystem** - Works with remark, rehype and seamlessly integrates with [react-markdown][react-markdown] and MDX ## Installation @@ -28,6 +29,54 @@ pnpm add remark-github-markdown-alerts bun add remark-github-markdown-alerts ``` +## Rendering Modes + +This plugin automatically detects your rendering environment and optimizes output accordingly: + +### 🔄 Auto-Detection (Recommended) + +The plugin automatically chooses the optimal rendering mode: + +- **HTML Mode**: Traditional remark → rehype → HTML pipelines +- **Component Mode**: react-markdown, MDX, and component-based systems + +```ts +import { remarkGitHubAlerts } from 'remark-github-markdown-alerts' + +// Auto-detects the right mode for your setup +remark().use(remarkGitHubAlerts) +``` + +### 🎯 Manual Mode Selection + +Override auto-detection when needed: + +```ts +import { remarkGitHubAlerts } from 'remark-github-markdown-alerts' + +// Force HTML mode (traditional pipelines) +remark().use(remarkGitHubAlerts, { mode: 'html' }) + +// Force component mode (react-markdown, MDX) +remark().use(remarkGitHubAlerts, { mode: 'component' }) + +// Auto-detection (default) +remark().use(remarkGitHubAlerts, { mode: 'auto' }) +``` + +### 📋 Mode Detection Logic + +**Component mode** is automatically triggered when: +- Using with `react-markdown` +- Processing `.mdx` files +- File data contains `{ mdx: true }` +- File data contains `{ allowDangerousHtml: false }` + +**HTML mode** is used for: +- Traditional remark → rehype → HTML pipelines +- Static site generators +- Server-side rendering without components + ## Usage ### With remark @@ -53,7 +102,6 @@ const result = await processor.process(markdown) console.log(result.toString()) ``` - ### With React Server Components and custom icons Using [`react-markdown`][react-markdown] and [`common-tags`][common-tags]'s `html` helper, example code in Next.js application: @@ -141,7 +189,7 @@ const result = await processor.process('> [!IMPORTANT]\\n> Critical information ## Configuration -The plugin accepts an options object with two main sections: +The plugin accepts an options object with the following sections: ### 🎛️ Global Configuration (`defaultConfig`) @@ -152,9 +200,12 @@ import { remarkGitHubAlerts } from 'remark-github-markdown-alerts' const options = { defaultConfig: { + // General options + mode: "auto" // Rendering mode selection + // 🎨 CSS Class Names classNames: { - container: 'alert', // Main wrapper class + container: 'alert', // Main wrapper class icon: 'alert-icon', // Icon container class title: 'alert-title', // Title/header class content: 'alert-content' // Content body class @@ -203,6 +254,15 @@ const processor = remark().use(remarkGitHubAlerts, options) ### 📋 Configuration Reference +#### Plugin Options + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| **Rendering** | | | | +| `mode` | `'auto' \| 'html' \| 'component'` | `'auto'` | Rendering mode selection | +| `defaultConfig` | `PartialDeep` | See below | Global configuration for all alerts | +| `alerts` | `AlertsConfig` | `{}` | Alert-specific configuration overrides | + #### Default Configuration Options | Property | Type | Default | Description | @@ -235,7 +295,11 @@ const processor = remark().use(remarkGitHubAlerts, options) > [!TIP] > Alert-specific configurations merge with the default config, so you only need to specify the properties you want to override. -## Example output +## Output Differences by Mode + +### HTML Mode Output + +Traditional HTML structure optimized for static sites and server-side rendering: ```html
@@ -249,6 +313,22 @@ const processor = remark().use(remarkGitHubAlerts, options)
``` +### Component Mode Output + +Enhanced structure optimized for react-markdown and MDX with additional metadata: + +```html +
+
+ + Note +
+
+

Your content here

+
+
+``` + # License The MIT License diff --git a/src/index.ts b/src/index.ts index d4023c7..cd886eb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import type { Blockquote, Html, Paragraph, PhrasingContent, Root, Text } from 'm import type { PartialDeep } from 'type-fest' import type { Plugin } from 'unified' import { visit } from 'unist-util-visit' +import type { VFile } from 'vfile' type HtmlElement = | 'html' @@ -143,9 +144,12 @@ export type AlertsConfig = { caution?: PartialDeep } +export type RenderMode = 'html' | 'component' | 'auto' + export type RemarkGitHubAlertsOptions = { alerts?: AlertsConfig defaultConfig?: PartialDeep + mode?: RenderMode } const ALERT_TYPES = ['NOTE', 'TIP', 'IMPORTANT', 'WARNING', 'CAUTION'] as const @@ -228,6 +232,26 @@ function escapeHtml(text: string): string { return result } +function detectRenderMode(file?: VFile): RenderMode { + if (!file) return 'html' + + const data = file.data || {} + + if (data['reactMarkdown'] === true) { + return 'component' + } + + if (data['mdx'] === true || file.extname === '.mdx') { + return 'component' + } + + if (data['allowDangerousHtml'] === false) { + return 'component' + } + + return 'html' +} + function extractTextContent(node: PhrasingContent): string { if (node.type === 'text') { return node.value @@ -238,6 +262,70 @@ function extractTextContent(node: PhrasingContent): string { return '' } +function createAlertComponent( + type: AlertType, + title: string, + children: Blockquote['children'], + config: AlertConfig +): Record { + const alertTypeKey = type.toLowerCase() as Lowercase + const containerClasses = [ + config.classNames.container, + `${config.classNames.container}-${alertTypeKey}`, + ] + .filter(Boolean) + .join(' ') + + const iconChildren = config.iconElementHtml + ? [{ type: 'text', value: config.iconElementHtml }] + : [] + + return { + type: config.tags.container, + data: { + hName: config.tags.container, + hProperties: { + className: containerClasses, + 'data-alert-type': alertTypeKey, + }, + }, + children: [ + { + type: config.tags.title, + data: { + hName: config.tags.title, + hProperties: { + className: config.classNames.title, + }, + }, + children: [ + { + type: config.tags.icon, + data: { + hName: config.tags.icon, + hProperties: { + className: config.classNames.icon, + }, + }, + children: iconChildren, + }, + { type: 'text', value: title }, + ], + }, + { + type: config.tags.content, + data: { + hName: config.tags.content, + hProperties: { + className: config.classNames.content, + }, + }, + children: children, + }, + ], + } +} + function createAlertHtml( type: AlertType, title: string, @@ -288,21 +376,7 @@ function mergeConfig( } } -export function processBlockquote( - node: Blockquote, - index: number | undefined, - parent: unknown, - baseConfig: AlertConfig, - alerts: AlertsConfig -): boolean { - if (!parent || typeof index !== 'number') return false - - const alertInfo = isAlertBlockquote(node) - if (!alertInfo.isAlert || !alertInfo.type || !alertInfo.title) return false - - const alertTypeKey = alertInfo.type.toLowerCase() as keyof AlertsConfig - const alertConfig = mergeConfig(baseConfig, alerts[alertTypeKey]) - +function processBlockquoteContent(node: Blockquote): Blockquote['children'] { const firstParagraph = node.children[0] as Paragraph const firstTextNode = firstParagraph.children[0] as Text const alertDeclarationMatch = firstTextNode.value.match( @@ -324,6 +398,61 @@ export function processBlockquote( } } + return node.children +} + +export function processBlockquoteAsComponent( + node: Blockquote, + index: number | undefined, + parent: unknown, + baseConfig: AlertConfig, + alerts: AlertsConfig +): boolean { + if (!parent || typeof index !== 'number') return false + + const alertInfo = isAlertBlockquote(node) + if (!alertInfo.isAlert || !alertInfo.type || !alertInfo.title) return false + + const alertTypeKey = alertInfo.type.toLowerCase() as keyof AlertsConfig + const alertConfig = mergeConfig(baseConfig, alerts[alertTypeKey]) + + const processedChildren = processBlockquoteContent(node) + const componentNode = createAlertComponent( + alertInfo.type, + alertInfo.title, + processedChildren, + alertConfig + ) + + if ( + parent && + typeof parent === 'object' && + 'children' in parent && + Array.isArray(parent.children) + ) { + parent.children[index] = componentNode as unknown as Blockquote + } + + return true +} + +export function processBlockquoteAsHtml( + node: Blockquote, + index: number | undefined, + parent: unknown, + baseConfig: AlertConfig, + alerts: AlertsConfig +): boolean { + if (!parent || typeof index !== 'number') return false + + const alertInfo = isAlertBlockquote(node) + if (!alertInfo.isAlert || !alertInfo.type || !alertInfo.title) return false + + const alertTypeKey = alertInfo.type.toLowerCase() as keyof AlertsConfig + const alertConfig = mergeConfig(baseConfig, alerts[alertTypeKey]) + + processBlockquoteContent(node) + const contentHtml = node.children .map(child => { if (child.type === 'paragraph') { @@ -353,13 +482,29 @@ export function processBlockquote( return true } +export function processBlockquote( + node: Blockquote, + index: number | undefined, + parent: unknown, + baseConfig: AlertConfig, + alerts: AlertsConfig, + mode: RenderMode = 'html' +): boolean { + if (mode === 'component') { + return processBlockquoteAsComponent(node, index, parent, baseConfig, alerts) + } + return processBlockquoteAsHtml(node, index, parent, baseConfig, alerts) +} + export const remarkGitHubAlerts: Plugin<[RemarkGitHubAlertsOptions?], Root> = (options = {}) => { - const { alerts = {}, defaultConfig } = options + const { alerts = {}, defaultConfig, mode = 'auto' } = options const baseConfig = mergeConfig(DEFAULT_CONFIG, defaultConfig) - return tree => { + return (tree, file) => { + const renderMode = mode === 'auto' ? detectRenderMode(file) : mode + visit(tree, 'blockquote', (node: Blockquote, index, parent) => { - processBlockquote(node, index, parent, baseConfig, alerts) + processBlockquote(node, index, parent, baseConfig, alerts, renderMode) }) return tree diff --git a/test/index.test.ts b/test/index.test.ts index 5ada6c8..41bed48 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,9 +1,15 @@ -import type { Blockquote, Root } from 'mdast' +import type { Blockquote } from 'mdast' import rehypeStringify from 'rehype-stringify' import { remark } from 'remark' import remarkRehype from 'remark-rehype' -import { describe, expect, it, vi } from 'vitest' -import { processBlockquote, type RemarkGitHubAlertsOptions, remarkGitHubAlerts } from '../src/index' +import { describe, expect, it } from 'vitest' +import { + processBlockquote, + processBlockquoteAsComponent, + processBlockquoteAsHtml, + type RemarkGitHubAlertsOptions, + remarkGitHubAlerts, +} from '../src/index' function processMarkdown(markdown: string, options?: RemarkGitHubAlertsOptions) { return remark() @@ -14,8 +20,113 @@ function processMarkdown(markdown: string, options?: RemarkGitHubAlertsOptions) .then(result => result.toString()) } +function processMarkdownComponent(markdown: string, options?: RemarkGitHubAlertsOptions) { + return remark() + .use(remarkGitHubAlerts, { ...options, mode: 'component' }) + .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeStringify, { allowDangerousHtml: true }) + .process(markdown) + .then(result => result.toString()) +} + +function processMarkdownWithFileData( + markdown: string, + fileData: Record, + options?: RemarkGitHubAlertsOptions +) { + return remark() + .use(remarkGitHubAlerts, options) + .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeStringify, { allowDangerousHtml: true }) + .process({ value: markdown, data: fileData }) + .then(result => result.toString()) +} + describe('remarkGitHubAlerts', () => { - describe('basic alert types', () => { + describe('mode detection', () => { + it('should detect component mode from reactMarkdown file data', async () => { + const markdown = `> [!NOTE] +> This is a note alert.` + const result = await processMarkdownWithFileData(markdown, { reactMarkdown: true }) + + expect(result).toContain('data-alert-type="note"') + expect(result).toContain('class="markdown-alert') + }) + + it('should detect component mode from mdx file data', async () => { + const markdown = `> [!TIP] +> This is a tip.` + const result = await processMarkdownWithFileData(markdown, { mdx: true }) + + expect(result).toContain('data-alert-type="tip"') + }) + + it('should detect component mode from .mdx file extension', async () => { + const markdown = `> [!WARNING] +> This is a warning.` + const result = await remark() + .use(remarkGitHubAlerts) + .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeStringify, { allowDangerousHtml: true }) + .process({ value: markdown, path: 'test.mdx' }) + .then(result => result.toString()) + + expect(result).toContain('data-alert-type="warning"') + }) + + it('should detect component mode when allowDangerousHtml is false', async () => { + const markdown = `> [!IMPORTANT] +> This is important.` + const result = await processMarkdownWithFileData(markdown, { allowDangerousHtml: false }) + + expect(result).toContain('data-alert-type="important"') + }) + + it('should default to html mode when no special file data is present', async () => { + const markdown = `> [!CAUTION] +> This is a caution.` + const result = await processMarkdown(markdown) + + expect(result).not.toContain('data-alert-type') + expect(result).not.toContain('className') + expect(result).toContain('class="markdown-alert') + }) + }) + + describe('explicit mode configuration', () => { + it('should use component mode when explicitly set', async () => { + const markdown = `> [!NOTE] +> Component mode test.` + const result = await processMarkdownComponent(markdown) + + expect(result).toContain('data-alert-type="note"') + expect(result).toContain('class="markdown-alert') + }) + + it('should use html mode when explicitly set', async () => { + const markdown = `> [!NOTE] +> HTML mode test.` + const result = await processMarkdown(markdown, { mode: 'html' }) + + expect(result).not.toContain('data-alert-type') + expect(result).not.toContain('className') + expect(result).toContain('class="markdown-alert') + }) + + it('should override auto-detection when mode is explicitly set', async () => { + const markdown = `> [!NOTE] +> Override test.` + const result = await processMarkdownWithFileData( + markdown, + { reactMarkdown: true }, + { mode: 'html' } + ) + + expect(result).not.toContain('data-alert-type') + expect(result).toContain('class="markdown-alert') + }) + }) + describe('basic alert types (HTML mode)', () => { it('should convert NOTE alert', async () => { const markdown = `> [!NOTE] > This is a note alert.` @@ -70,7 +181,67 @@ describe('remarkGitHubAlerts', () => { }) }) - describe('custom titles', () => { + describe('basic alert types (Component mode)', () => { + it('should convert NOTE alert to component structure', async () => { + const markdown = `> [!NOTE] +> This is a note alert.` + const result = await processMarkdownComponent(markdown) + + expect(result).toContain('data-alert-type="note"') + expect(result).toContain('class="markdown-alert markdown-alert-note"') + expect(result).toContain('class="markdown-alert-title"') + expect(result).toContain('class="markdown-alert-icon"') + expect(result).toContain('class="markdown-alert-content"') + expect(result).toContain('Note') + expect(result).toContain('This is a note alert.') + }) + + it('should convert TIP alert to component structure', async () => { + const markdown = `> [!TIP] +> This is a tip alert.` + const result = await processMarkdownComponent(markdown) + + expect(result).toContain('data-alert-type="tip"') + expect(result).toContain('class="markdown-alert markdown-alert-tip"') + expect(result).toContain('Tip') + expect(result).toContain('This is a tip alert.') + }) + + it('should convert IMPORTANT alert to component structure', async () => { + const markdown = `> [!IMPORTANT] +> This is an important alert.` + const result = await processMarkdownComponent(markdown) + + expect(result).toContain('data-alert-type="important"') + expect(result).toContain('class="markdown-alert markdown-alert-important"') + expect(result).toContain('Important') + expect(result).toContain('This is an important alert.') + }) + + it('should convert WARNING alert to component structure', async () => { + const markdown = `> [!WARNING] +> This is a warning alert.` + const result = await processMarkdownComponent(markdown) + + expect(result).toContain('data-alert-type="warning"') + expect(result).toContain('class="markdown-alert markdown-alert-warning"') + expect(result).toContain('Warning') + expect(result).toContain('This is a warning alert.') + }) + + it('should convert CAUTION alert to component structure', async () => { + const markdown = `> [!CAUTION] +> This is a caution alert.` + const result = await processMarkdownComponent(markdown) + + expect(result).toContain('data-alert-type="caution"') + expect(result).toContain('class="markdown-alert markdown-alert-caution"') + expect(result).toContain('Caution') + expect(result).toContain('This is a caution alert.') + }) + }) + + describe('custom titles (HTML mode)', () => { it('should use custom title when provided', async () => { const markdown = `> [!NOTE] Custom Title > This note has a custom title.` @@ -91,6 +262,29 @@ describe('remarkGitHubAlerts', () => { }) }) + describe('custom titles (Component mode)', () => { + it('should use custom title when provided', async () => { + const markdown = `> [!NOTE] Custom Title +> This note has a custom title.` + const result = await processMarkdownComponent(markdown) + + expect(result).toContain('Custom Title') + expect(result).not.toContain('Note') + expect(result).toContain('This note has a custom title.') + expect(result).toContain('data-alert-type="note"') + }) + + it('should handle custom title with multiple words', async () => { + const markdown = `> [!TIP] Very Important Tip Here +> This is the content.` + const result = await processMarkdownComponent(markdown) + + expect(result).toContain('Very Important Tip Here') + expect(result).toContain('This is the content.') + expect(result).toContain('data-alert-type="tip"') + }) + }) + describe('multi-line content', () => { it('should handle multi-line alert content', async () => { const markdown = `> [!NOTE] @@ -134,7 +328,7 @@ describe('remarkGitHubAlerts', () => { }) }) - describe('configuration options', () => { + describe('configuration options (HTML mode)', () => { it('should apply custom class names', async () => { const markdown = `> [!NOTE] > Test content.` @@ -268,6 +462,110 @@ describe('remarkGitHubAlerts', () => { }) }) + describe('configuration options (Component mode)', () => { + it('should apply custom class names in component mode', async () => { + const markdown = `> [!NOTE] +> Test content.` + const options: RemarkGitHubAlertsOptions = { + mode: 'component', + defaultConfig: { + classNames: { + container: 'custom-class another-class', + }, + }, + } + const result = await processMarkdown(markdown, options) + + expect(result).toContain('class="custom-class') + expect(result).toContain('another-class') + expect(result).toContain('data-alert-type="note"') + }) + + it('should apply custom container class name in component mode', async () => { + const markdown = `> [!TIP] +> Test content.` + const options: RemarkGitHubAlertsOptions = { + mode: 'component', + defaultConfig: { + classNames: { + container: 'custom-alert', + }, + }, + } + const result = await processMarkdown(markdown, options) + + expect(result).toContain('class="custom-alert custom-alert-tip"') + expect(result).toContain('class="markdown-alert-title"') + expect(result).toContain('class="markdown-alert-content"') + }) + + it('should apply alert-specific configuration in component mode', async () => { + const markdown = `> [!NOTE] +> Test content.` + const options: RemarkGitHubAlertsOptions = { + mode: 'component', + alerts: { + note: { + classNames: { + container: 'note-container', + title: 'note-title', + content: 'note-content', + icon: 'note-icon', + }, + }, + }, + } + const result = await processMarkdown(markdown, options) + + expect(result).toContain('class="note-container note-container-note"') + expect(result).toContain('class="note-title"') + expect(result).toContain('class="note-content"') + expect(result).toContain('class="note-icon"') + }) + + it('should apply custom HTML tags in component mode', async () => { + const markdown = `> [!NOTE] +> Test content.` + const options: RemarkGitHubAlertsOptions = { + mode: 'component', + defaultConfig: { + tags: { + container: 'section', + title: 'h3', + icon: 'i', + content: 'article', + }, + }, + } + const result = await processMarkdown(markdown, options) + + expect(result).toContain('
') + expect(result).toContain('') + expect(result).toContain('') + expect(result).toContain('') + }) + + it('should apply custom icon HTML in component mode', async () => { + const markdown = `> [!TIP] +> Test content.` + const options: RemarkGitHubAlertsOptions = { + mode: 'component', + alerts: { + tip: { + iconElementHtml: 'tip-icon', + }, + }, + } + const result = await processMarkdown(markdown, options) + + expect(result).toContain('tip-icon') + }) + }) + describe('edge cases and error handling', () => { it('should not convert regular blockquotes', async () => { const markdown = `> This is a regular blockquote @@ -399,7 +697,7 @@ More regular text. expect(result).toContain('italic text') }) - it('should handle edge case with invalid parent or index', () => { + it('should handle edge case with invalid parent or index in HTML mode', () => { // This tests the defensive code path directly const blockquoteNode: Blockquote = { type: 'blockquote', @@ -433,13 +731,24 @@ More regular text. } // Test with null parent - should return false - expect(processBlockquote(blockquoteNode, 0, null, mockConfig, {})).toBe(false) + expect(processBlockquote(blockquoteNode, 0, null, mockConfig, {}, 'html')).toBe(false) // Test with undefined index - should return false - expect(processBlockquote(blockquoteNode, undefined as any, {}, mockConfig, {})).toBe(false) + expect( + processBlockquote( + blockquoteNode, + undefined as unknown as number, + {}, + mockConfig, + {}, + 'html' + ) + ).toBe(false) // Test with string index instead of number - should return false - expect(processBlockquote(blockquoteNode, '0' as any, {}, mockConfig, {})).toBe(false) + expect( + processBlockquote(blockquoteNode, '0' as unknown as number, {}, mockConfig, {}, 'html') + ).toBe(false) // Test the edge case where text.split('\n')[0] is falsy (empty string) const emptyTextBlockquote: Blockquote = { @@ -458,7 +767,101 @@ More regular text. } const mockParent = { children: [emptyTextBlockquote] } - expect(processBlockquote(emptyTextBlockquote, 0, mockParent, mockConfig, {})).toBe(false) + expect(processBlockquote(emptyTextBlockquote, 0, mockParent, mockConfig, {}, 'html')).toBe( + false + ) + }) + + it('should handle edge case with invalid parent or index in component mode', () => { + const blockquoteNode: Blockquote = { + type: 'blockquote', + children: [ + { + type: 'paragraph', + children: [ + { + type: 'text', + value: '[!NOTE] Test', + }, + ], + }, + ], + } + + const mockConfig = { + iconElementHtml: '', + tags: { + container: 'div' as const, + icon: 'span' as const, + title: 'div' as const, + content: 'div' as const, + }, + classNames: { + container: 'test', + icon: 'test-icon', + title: 'test-title', + content: 'test-content', + }, + } + + // Test with null parent - should return false + expect(processBlockquote(blockquoteNode, 0, null, mockConfig, {}, 'component')).toBe(false) + + // Test with undefined index - should return false + expect( + processBlockquote( + blockquoteNode, + undefined as unknown as number, + {}, + mockConfig, + {}, + 'component' + ) + ).toBe(false) + + // Test with string index instead of number - should return false + expect( + processBlockquote(blockquoteNode, '0' as unknown as number, {}, mockConfig, {}, 'component') + ).toBe(false) + }) + + it('should test individual processing functions', () => { + const blockquoteNode: Blockquote = { + type: 'blockquote', + children: [ + { + type: 'paragraph', + children: [ + { + type: 'text', + value: '[!NOTE] Test', + }, + ], + }, + ], + } + + const mockConfig = { + iconElementHtml: '', + tags: { + container: 'div' as const, + icon: 'span' as const, + title: 'div' as const, + content: 'div' as const, + }, + classNames: { + container: 'test', + icon: 'test-icon', + title: 'test-title', + content: 'test-content', + }, + } + + // Test HTML processing function directly + expect(processBlockquoteAsHtml(blockquoteNode, 0, null, mockConfig, {})).toBe(false) + + // Test component processing function directly + expect(processBlockquoteAsComponent(blockquoteNode, 0, null, mockConfig, {})).toBe(false) }) }) })