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
122 changes: 80 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
# MDX Renderer [@m2d/react-markdown] <img src="https://raw.githubusercontent.com/mayank1513/mayank1513/main/popper.png" style="height: 40px"/>
# MDX Renderer [`@m2d/react-markdown`] <img src="https://raw.githubusercontent.com/mayank1513/mayank1513/main/popper.png" style="height: 40px"/>

[![test](https://github.com/md2docx/react-markdown/actions/workflows/test.yml/badge.svg)](https://github.com/md2docx/react-markdown/actions/workflows/test.yml) [![Maintainability](https://api.codeclimate.com/v1/badges/aa896ec14c570f3bb274/maintainability)](https://codeclimate.com/github/md2docx/react-markdown/maintainability) [![codecov](https://codecov.io/gh/md2docx/react-markdown/graph/badge.svg)](https://codecov.io/gh/md2docx/react-markdown) [![Version](https://img.shields.io/npm/v/@m2d/react-markdown.svg?colorB=green)](https://www.npmjs.com/package/@m2d/react-markdown) [![Downloads](https://img.jsdelivr.com/img.shields.io/npm/d18m/@m2d/react-markdown.svg)](https://www.npmjs.com/package/@m2d/react-markdown) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/@m2d/react-markdown) [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/from-referrer/)
[![test](https://github.com/md2docx/react-markdown/actions/workflows/test.yml/badge.svg)](https://github.com/md2docx/react-markdown/actions/workflows/test.yml)
[![Maintainability](https://api.codeclimate.com/v1/badges/aa896ec14c570f3bb274/maintainability)](https://codeclimate.com/github/md2docx/react-markdown/maintainability)
[![codecov](https://codecov.io/gh/md2docx/react-markdown/graph/badge.svg)](https://codecov.io/gh/md2docx/react-markdown)
[![Version](https://img.shields.io/npm/v/@m2d/react-markdown.svg?colorB=green)](https://www.npmjs.com/package/@m2d/react-markdown)
[![Downloads](https://img.jsdelivr.com/img.shields.io/npm/d18m/@m2d/react-markdown.svg)](https://www.npmjs.com/package/@m2d/react-markdown)
![npm bundle size](https://img.shields.io/bundlephobia/minzip/@m2d/react-markdown)

> ✨ A modern, JSX-compatible, SSR-ready Markdown renderer for React with full access to MDAST & HAST trees for tools like `mdast2docx`.
> ✨ A modern, SSR-compatible Markdown renderer for React with full MDAST/HAST access — built for **customization**, **performance**, and **document generation** - **docx/pdf**.

---

## 🔥 Why mdx-render?
## 🔥 Why `@m2d/react-markdown`?

`mdx-render` goes beyond traditional React Markdown libraries by focusing on:
`@m2d/react-markdown` goes beyond traditional React Markdown libraries by focusing on:

- ✅ **Server-side rendering (SSR)** without hooks
- ✅ **Full JSX children support** (not just strings)
Expand All @@ -17,9 +22,21 @@
- ✅ **Custom component overrides** per tag
- ✅ **Integration with tools like [`mdast2docx`](https://github.com/md2docx/mdast2docx)**

Compared to `react-markdown`, this library offers:

| Feature | `@m2d/react-markdown` ✅ | `react-markdown` ❌ |
| ----------------------------------- | ------------------------ | ------------------- |
| Full JSX support (not just strings) | ✅ | ❌ |
| SSR-safe (no hooks) | ✅ | ⚠️ (limited) |
| MDAST + HAST access via `astRef` | ✅ | ❌ |
| Component-level overrides | ✅ | ✅ |
| Unified plugin support | ✅ | ✅ |
| Tiny bundle (minzipped) | **~35 kB** | ~45 kB |
| Built-in DOCX-friendly AST output | ✅ | ❌ |

---

## 🚀 Installation
## 📦 Installation

```bash
pnpm add @m2d/react-markdown
Expand All @@ -39,10 +56,30 @@ yarn add @m2d/react-markdown

---

## ⚡ Quick Example
## 🚀 Server vs Client

By default, this package is SSR-safe and has **no client-specific hooks**.

### ✅ Server (default):

```tsx
import { Md } from "@m2d/react-markdown";
```

### 🔁 Client (for dynamic reactivity/memoization):

```tsx
import { Md } from "mdx-render";
import { Md } from "@m2d/react-markdown/client";
```

This version supports client-side behavior with memoization and dynamic JSX rendering.

---

## ⚡ Example: Rendering + Exporting DOCX

```tsx
import { Md } from "@m2d/react-markdown/client";
import { toDocx } from "mdast2docx";
import { useRef } from "react";

Expand All @@ -55,7 +92,7 @@ export default function Page() {
<button
onClick={() => {
const doc = toDocx(astRef.current[0].mdast);
// Export DOCX, or save
// Save or download doc
}}>
Export to DOCX
</button>
Expand All @@ -64,48 +101,50 @@ export default function Page() {
}
```

> Note for Server Component use you can replace useRef with custom ref object `const astRef = {current: undefined} as AstRef`

---

## 🧠 JSX-Aware Parsing

Unlike other libraries, this renderer supports **JSX as children**, which means you can nest Markdown inside arbitrary components:
Unlike most markdown renderers, `@m2d/react-markdown` supports **arbitrary JSX as children**:

```tsx
<Md>
<section>{`# Title\n\nContent.`}</section>
<article>{"# Markdown Heading\n\nSome **rich** content."}</article>
</Md>
```

> Note: `astRef.current` is an array — one entry per Markdown segment.
> Each entry contains `{ mdast, hast }` for fine-grained control.
> `astRef.current` is an array — one per Markdown string — each with `{ mdast, hast }`.

---

## ✨ Component Overrides

Override default HTML rendering with your own components:
## 🎨 Component Overrides

```tsx
import { Md, Unwrap, Omit } from "@m2d/react-markdown";

<Md
components={{
code: (props) => <CodeWithHighlights {...props} />
em: Unwrap, // Renders <em> content without tags
blockquote: Omit, // Removes <blockquote> completely
em: Unwrap,
blockquote: Omit,
code: props => <CodeBlock {...props} />,
}}>
{`*This will be unwrapped*\n\n> This will be removed!`}
</Md>
{`*em is unwrapped*\n\n> blockquote is removed`}
</Md>;
```

Use the built-in helpers:

- `Unwrap` – renders children, ignores tag & props.
- `Omit` – removes the element and its content entirely.
- `Unwrap` – renders only children
- `Omit` – removes element and content entirely
- `CodeBlock` - it is your custom component

---

## 🧩 Plugin Support
## 🔌 Plugin Support (Unified)

Use any `remark` or `rehype` plugins with full flexibility:
Use any `remark` or `rehype` plugin:

```tsx
<Md remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeSlug, rehypeAutolinkHeadings]}>
Expand All @@ -115,45 +154,44 @@ Use any `remark` or `rehype` plugins with full flexibility:

---

## 📦 astRef: MDAST + HAST Access
## 📂 Accessing MDAST + HAST

```ts
type astRef = {
current: { mdast: Root; hast: HastRoot }[];
};
```

Each markdown block is processed independently to allow full JSX flexibility.
You can access all parsed trees via `astRef.current`, ideal for:
Useful for:

- DOCX/PDF generation (`mdast2docx`)
- Markdown linting or analytics
- AST-aware transformations
- 📄 DOCX export (`mdast2docx`)
- 🧪 AST testing or analysis
- 🛠️ Custom tree manipulation

---

## 🧭 Roadmap

- [ ] 🔄 Merge surrounding JSX + `<Md>` blocks into unified MDAST/HAST
- [x] 🧪 Add test utilities for structural validation
- [x] 📚 Provide Next.js examples with DOCX export
- [ ] 🔄 Merge JSX + `<Md>` segments into unified AST
- [x] 🧪 Structural test utilities
- [x] 🧑‍🏫 Next.js + DOCX example

---

## 📘 Related Projects
## 🌍 Related Projects

- [mdast2docx](https://github.com/md2docx/mdast2docx) – Convert MDAST to Word (.docx)
- [unifiedjs](https://unifiedjs.com/) – Syntax tree processing toolkit
- [react-markdown](https://github.com/remarkjs/react-markdown) – A simpler but less flexible Markdown renderer
- [`mdast2docx`](https://github.com/md2docx/mdast2docx) – Convert MDAST → `.docx`
- [`unified`](https://unifiedjs.com/) – Syntax tree ecosystem
- [`react-markdown`](https://github.com/remarkjs/react-markdown) – Popular alternative (less customizable)

---

## License
## 📘 License

This library is licensed under the MPL-2.0 open-source license.
Licensed under the [MPL-2.0](https://www.mozilla.org/en-US/MPL/2.0/).

> <img src="https://raw.githubusercontent.com/mayank1513/mayank1513/main/popper.png" style="height: 20px"/> Please enroll in [our courses](https://mayank-chaudhari.vercel.app/courses) or [sponsor](https://github.com/sponsors/mayank1513) our work.
> 💡 Want to support this project? [Sponsor](https://github.com/sponsors/mayank1513) or check out our [courses](https://mayank-chaudhari.vercel.app/courses)!

---

<p align="center" style="text-align:center">with 💖 by <a href="https://mayank-chaudhari.vercel.app" target="_blank">Mayank Kumar Chaudhari</a></p>
<p align="center" style="text-align:center">Built with ❤️ by <a href="https://mayank-chaudhari.vercel.app" target="_blank">Mayank Kumar Chaudhari</a></p>
33 changes: 33 additions & 0 deletions lib/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
# @m2d/react-markdown

## 0.2.0

### Minor Changes

- e1d4106: Add client-side `<Md />` component and refactor core utilities.

- **Client-side `<Md />` component**:

- New entrypoints `@m2d/react-markdown/client` and `@m2d/react-markdown/dist/client` for a memoized, client-optimized Markdown renderer.
- uses memoization for client side optimizations
- Supports full JSX children, SSR-safe, and dynamic and optimized reactivity.

- **Refactor core utilities**:

- Move all shared types and helpers (e.g., `ComponentProps`, `AstRef`, `Markdown`, `uuid`, etc.) to `lib/src/utils.tsx`.
- Remove duplicated code and unify server/client logic.

- **Testing**:

- Add comprehensive tests for the client `<Md />` component.

- **Package exports**:

- Update `lib/package.json` to expose new client entrypoints and dependencies.

- **Other**:
- Update coverage exclusions in `vitest.config.mts`.
- Minor internal cleanups and improved type safety.

***

This release introduces a modern, SSR-safe, and client-optimized Markdown renderer with unified logic and improved maintainability.

## 0.1.1

### Patch Changes
Expand Down
27 changes: 20 additions & 7 deletions lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@m2d/react-markdown",
"author": "Mayank Kumar Chaudhari (https://mayank-chaudhari.vercel.app)",
"private": false,
"version": "0.1.1",
"version": "0.2.0",
"description": "A modern, SSR-friendly React Markdown renderer that preserves the MDAST tree for reuse (e.g., mdast2docx), supports full JSX children, unified plugins, and component overrides.",
"license": "MPL-2.0",
"main": "./dist/index.js",
Expand All @@ -21,6 +21,16 @@
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./client": {
"types": "./dist/client/index.d.ts",
"import": "./dist/client/index.mjs",
"require": "./dist/client/index.js"
},
"./dist/client": {
"types": "./dist/client/index.d.ts",
"import": "./dist/client/index.mjs",
"require": "./dist/client/index.js"
},
"./server": {
"types": "./dist/server/index.d.ts",
"import": "./dist/server/index.mjs",
Expand Down Expand Up @@ -85,14 +95,22 @@
"esbuild-plugin-rdi": "^0.0.0",
"esbuild-plugin-react18": "0.2.6",
"esbuild-plugin-react18-css": "^0.0.4",
"fast-deep-equal": "^3.1.3",
"jsdom": "^26.1.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"tsup": "^8.5.0",
"typescript": "^5.8.3",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.1.4"
},
"dependencies": {
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"unified": "^11.0.5"
},
"peerDependencies": {
"@types/react": ">=16.8",
"next": ">=10",
Expand Down Expand Up @@ -145,10 +163,5 @@
"cutting-edge",
"compatibility",
"seamless integration"
],
"dependencies": {
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"unified": "^11.0.5"
}
]
}
10 changes: 10 additions & 0 deletions lib/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"use client";

/**
* Server components and client components need to be exported from separate files as
* directive on top of the file from which component is imported takes effect.
* i.e., server component re-exported from file with "use client" will behave as client component
*/

// client component exports
export * from "./md";
4 changes: 4 additions & 0 deletions lib/src/client/md/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"use client";

// component exports
export * from "./md";
59 changes: 59 additions & 0 deletions lib/src/client/md/md.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, test } from "vitest";
import { Md } from "./md";
import React from "react";
import { uuid } from "../../utils";

describe.concurrent("md", () => {
afterEach(cleanup);

test("renders with custom className", ({ expect }) => {
const clx = "my-class";
const testId = "md-" + uuid();
render(<Md className={clx} data-testid={testId} />);
expect(screen.getByTestId(testId).classList).toContain(clx);
});

test("parses markdown", ({ expect }) => {
const testId = "md-" + uuid();
render(<Md data-testid={testId}>Hello **world**</Md>);
expect(screen.getByTestId(testId).textContent).toBe("Hello world");
});

test("renders children as JSX element", ({ expect }) => {
render(
<Md>
<span data-testid="child">Child</span>
</Md>,
);
expect(screen.getByTestId("child").textContent).toBe("Child");
});

test("renders nested JSX elements", ({ expect }) => {
render(
<Md data-testid={"md-" + uuid()}>
<div data-testid="outer">
<span data-testid="inner">Nested</span>
</div>
</Md>,
);
expect(screen.getByTestId("outer").textContent).toBe("Nested");
expect(screen.getByTestId("inner").textContent).toBe("Nested");
});

test("renders with no wrapper when no props and no wrapper provided", ({ expect }) => {
// Should use Fragment, so no extra DOM node
const { container } = render(<Md>Fragment content</Md>);
expect(container.textContent).toBe("Fragment content");
});

test("renders functional component children", ({ expect }) => {
const Fn = ({ children }: { children: React.ReactNode }) => <b data-testid="fn">{children}</b>;
render(
<Md>
<Fn>Bold</Fn>
</Md>,
);
expect(screen.getByTestId("fn").textContent).toBe("Bold");
});
});
Loading
Loading