diff --git a/lib/plugins/renderer/index.ts b/lib/plugins/renderer/index.ts index c8529c5a57..38117e61cf 100644 --- a/lib/plugins/renderer/index.ts +++ b/lib/plugins/renderer/index.ts @@ -21,4 +21,6 @@ export = (ctx: Hexo) => { renderer.register('njk', 'html', nunjucks, true); renderer.register('j2', 'html', nunjucks, true); + + renderer.register('mdx', 'html', require('./mdx'), false); }; diff --git a/lib/plugins/renderer/mdx.ts b/lib/plugins/renderer/mdx.ts new file mode 100644 index 0000000000..013b6e46b8 --- /dev/null +++ b/lib/plugins/renderer/mdx.ts @@ -0,0 +1,31 @@ +import { evaluate } from '@mdx-js/mdx'; +import { h, Fragment } from 'preact'; +import render from 'preact-render-to-string'; +import type { StoreFunctionData } from '../../extend/renderer'; + +async function mdxRenderer(data: StoreFunctionData): Promise { + const { text, path } = data; + + try { + // Evaluate MDX content with Preact JSX runtime + const { default: Content } = await evaluate(text, { + Fragment, + jsx: h, + jsxs: h, + development: false + }); + + // Render the MDX component to HTML string + const html = render(h(Content, {})); + + return html; + } catch (error) { + const fileInfo = path ? ` in ${path}` : ''; + throw new Error(`MDX compilation error${fileInfo}: ${error.message}\n${error.stack || ''}`); + } +} + +// Disable Nunjucks processing for MDX files +mdxRenderer.disableNunjucks = true; + +export = mdxRenderer; diff --git a/package.json b/package.json index 3157e5ea1a..966c51c772 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ ], "license": "MIT", "dependencies": { + "@mdx-js/mdx": "^3.1.1", "abbrev": "^3.0.0", "bluebird": "^3.7.2", "fast-archy": "^1.0.0", @@ -60,6 +61,8 @@ "moment-timezone": "^0.5.46", "nunjucks": "^3.2.4", "picocolors": "^1.1.1", + "preact": "^10.28.1", + "preact-render-to-string": "^6.6.5", "pretty-hrtime": "^1.0.3", "strip-ansi": "^7.1.0", "tildify": "^2.0.0", diff --git a/test/scripts/renderers/mdx.ts b/test/scripts/renderers/mdx.ts new file mode 100644 index 0000000000..06bbe19a81 --- /dev/null +++ b/test/scripts/renderers/mdx.ts @@ -0,0 +1,104 @@ +import r from '../../../lib/plugins/renderer/mdx'; + +describe('mdx', () => { + it('should render basic MDX content', async () => { + const result = await r({ text: '# Hello World' }); + result.should.include('

Hello World

'); + }); + + it('should render MDX with bold text', async () => { + const result = await r({ text: 'This is **bold** text.' }); + result.should.include('bold'); + }); + + it('should render MDX with links', async () => { + const result = await r({ text: '[Link](https://example.com)' }); + result.should.include('Link'); + }); + + it('should render MDX with multiple elements', async () => { + const mdxContent = `# Title + +This is a paragraph with **bold** and *italic* text. + +- List item 1 +- List item 2 + +[Link](https://example.com)`; + + const result = await r({ text: mdxContent }); + result.should.include('

Title

'); + result.should.include('bold'); + result.should.include('italic'); + result.should.include('
  • List item 1
  • '); + result.should.include('Link'); + }); + + it('should handle errors gracefully', async () => { + try { + // Invalid MDX syntax - JSX that can't be evaluated + await r({ text: ' { + r.disableNunjucks.should.be.true; + }); + + it('should render JSX elements', async () => { + const jsxContent = `# JSX Test + +
    + This is a **JSX element**. +
    `; + + const result = await r({ text: jsxContent }); + result.should.include('

    JSX Test

    '); + result.should.include('
    '); + result.should.include('JSX element'); + }); + + it('should render JSX with expressions', async () => { + const jsxWithExpressions = `# Dynamic Content + +
    + Year: {2024} +
    `; + + const result = await r({ text: jsxWithExpressions }); + result.should.include('

    Dynamic Content

    '); + result.should.include('
    '); + result.should.include('Year: 2024'); + }); + + it('should render JSX with inline styles', async () => { + const jsxWithStyles = `
    + Styled content +
    `; + + const result = await r({ text: jsxWithStyles }); + result.should.include('color:red'); + result.should.include('padding:10px'); + result.should.include('Styled content'); + }); + + it('should render custom components', async () => { + const customComponent = `export const Alert = ({children, type}) => ( +
    + {children} +
    +); + + + This is a **warning**! +`; + + const result = await r({ text: customComponent }); + result.should.include('alert-warning'); + result.should.include('warning'); + }); +});