Repository: mintlify/mdx
Branch: main
Commit: 439e0d177984
Files: 51
Total size: 74.1 KB
Directory structure:
gitextract_yzwoer2e/
├── .eslintrc.cjs
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .yarnrc.yml
├── examples/
│ ├── app-router/
│ │ ├── .eslintrc.json
│ │ ├── .gitignore
│ │ ├── app/
│ │ │ ├── globals.css
│ │ │ ├── layout.tsx
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ │ ├── examples/
│ │ │ └── highlight-example.mdx
│ │ ├── next.config.js
│ │ ├── package.json
│ │ ├── postcss.config.js
│ │ ├── readme.md
│ │ ├── tailwind.config.ts
│ │ └── tsconfig.json
│ └── pages-router/
│ ├── .eslintrc.json
│ ├── .gitignore
│ ├── eslint.config.mjs
│ ├── examples/
│ │ └── highlight-example.mdx
│ ├── next.config.js
│ ├── package.json
│ ├── pages/
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ └── index.tsx
│ ├── postcss.config.js
│ ├── readme.md
│ ├── styles/
│ │ └── globals.css
│ ├── tailwind.config.ts
│ └── tsconfig.json
├── package.json
├── packages/
│ └── mdx/
│ ├── package.json
│ ├── src/
│ │ ├── client/
│ │ │ ├── default.tsx
│ │ │ └── rsc.tsx
│ │ ├── index.ts
│ │ ├── plugins/
│ │ │ ├── index.ts
│ │ │ └── rehype/
│ │ │ ├── index.ts
│ │ │ ├── rehypeSyntaxHighlighting.ts
│ │ │ ├── shiki/
│ │ │ │ └── custom-language.ts
│ │ │ ├── shiki-constants.ts
│ │ │ ├── twoslash/
│ │ │ │ └── config.ts
│ │ │ └── utils.ts
│ │ ├── server/
│ │ │ └── index.ts
│ │ ├── types/
│ │ │ └── index.ts
│ │ └── ui/
│ │ ├── index.ts
│ │ └── popup.tsx
│ ├── tsconfig.build.json
│ └── tsconfig.json
└── readme.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintrc.cjs
================================================
module.exports = {
extends: ['@mintlify/eslint-config-typescript'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json',
},
ignorePatterns: ['.eslintrc.cjs', 'dist'],
overrides: [
{
files: ['*.js'],
extends: ['plugin:@typescript-eslint/disable-type-checked'],
},
],
};
================================================
FILE: .gitignore
================================================
# NPM
node_modules/
.eslintcache
yarn-error.log
# Output
dist/
# Misc
.DS_Store
# TypeScript
*.tsbuildinfo
# yarn
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
================================================
FILE: .prettierignore
================================================
/dist
/node_modules
.next
================================================
FILE: .prettierrc
================================================
"@mintlify/prettier-config/config.js"
================================================
FILE: .yarnrc.yml
================================================
nodeLinker: node-modules
================================================
FILE: examples/app-router/.eslintrc.json
================================================
{
"extends": "next/core-web-vitals"
}
================================================
FILE: examples/app-router/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
================================================
FILE: examples/app-router/app/globals.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--primary-light: 85 215 153;
}
}
/* modified from https://github.com/shikijs/shiki/blob/main/packages/twoslash/style-rich.css */
/* ===== Basic ===== */
:root {
--twoslash-border-color: #8888;
--twoslash-underline-color: currentColor;
--twoslash-highlighted-border: #c37d0d50;
--twoslash-highlighted-bg: #c37d0d20;
--twoslash-popup-bg: #f8f8f8;
--twoslash-popup-color: inherit;
--twoslash-popup-shadow: rgba(0, 0, 0, 0.08) 0px 1px 4px;
--twoslash-docs-color: #888;
--twoslash-docs-font: sans-serif;
--twoslash-code-font: inherit;
--twoslash-code-font-size: 1em;
--twoslash-matched-color: inherit;
--twoslash-unmatched-color: #888;
--twoslash-cursor-color: #8888;
--twoslash-error-color: #d45656;
--twoslash-error-bg: #d4565620;
--twoslash-warn-color: #c37d0d;
--twoslash-warn-bg: #c37d0d20;
--twoslash-tag-color: #3772cf;
--twoslash-tag-bg: #3772cf20;
--twoslash-tag-warn-color: var(--twoslash-warn-color);
--twoslash-tag-warn-bg: var(--twoslash-warn-bg);
--twoslash-tag-annotate-color: #1ba673;
--twoslash-tag-annotate-bg: #1ba67320;
--twoslash-text-size: 0.8rem;
--twoslash-docs-tag-style: italic;
}
/* Respect people's wishes to not have animations */
@media (prefers-reduced-motion: reduce) {
.twoslash * {
transition: none !important;
}
}
/* ===== Hover Info ===== */
.twoslash:hover .twoslash-hover {
border-color: var(--twoslash-underline-color);
}
.twoslash .twoslash-hover {
border-bottom: 1px dotted transparent;
transition-timing-function: ease;
transition: border-color 0.3s;
position: relative;
}
/* ===== Popup Override ===== */
.mint-twoslash-popover {
background: var(--twoslash-popup-bg);
color: var(--twoslash-popup-color);
border: 1px solid var(--twoslash-border-color);
border-radius: 4px;
pointer-events: auto;
text-align: left;
box-shadow: var(--twoslash-popup-shadow);
display: inline-flex;
flex-direction: column;
}
.mint-twoslash-popover-pre {
display: flex;
font-size: var(--twoslash-text-size);
font-family: var(--twoslash-code-font);
}
.mint-twoslash-popover:hover {
user-select: auto;
}
.twoslash .twoslash-popup-arrow {
display: none;
}
.twoslash-popup-code,
.twoslash-popup-error,
.twoslash-popup-docs {
padding: 6px 8px !important;
}
.mint-twoslash-popover .twoslash-popup-docs {
color: var(--twoslash-docs-color);
font-family: var(--twoslash-docs-font);
font-size: var(--twoslash-text-size);
max-width: unset;
}
.mint-twoslash-popover .twoslash-popup-error {
color: var(--twoslash-error-color);
background-color: var(--twoslash-error-bg);
font-family: var(--twoslash-docs-font);
font-size: var(--twoslash-text-size);
}
.mint-twoslash-popover .twoslash-popup-docs-tags {
display: flex;
flex-direction: column;
font-family: var(--twoslash-docs-font);
}
.mint-twoslash-popover .twoslash-popup-docs-tag-name {
margin-right: 0.5em;
font-style: var(--twoslash-docs-tag-style);
}
.mint-twoslash-popover .twoslash-popup-docs-tag-name {
font-family: var(--twoslash-code-font);
}
/* ===== Query Line ===== */
.mint-twoslash-popover .twoslash-query-line .twoslash-popup-container {
position: relative;
margin-bottom: 1.4em;
transform: translateY(0.6em);
}
/* ===== Error Line ===== */
.mint-twoslash-popover .twoslash-error-line {
position: relative;
background-color: var(--twoslash-error-bg);
border-left: 3px solid var(--twoslash-error-color);
color: var(--twoslash-error-color);
padding: 6px 12px;
margin: 0.2em 0;
min-width: 100%;
width: max-content;
}
.mint-twoslash-popover .twoslash-error-line.twoslash-error-level-warning {
background-color: var(--twoslash-warn-bg);
border-left: 3px solid var(--twoslash-warn-color);
color: var(--twoslash-warn-color);
}
.mint-twoslash-popover .twoslash-error {
background: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23c94824'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E") repeat-x bottom left;
padding-bottom: 2px;
}
.mint-twoslash-popover .twoslash-error.twoslash-error-level-warning {
background: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23c37d0d'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E") repeat-x bottom left;
padding-bottom: 2px;
}
/* ===== Completeions ===== */
.mint-twoslash-popover .twoslash-completion-cursor {
position: relative;
}
.mint-twoslash-popover .twoslash-completion-cursor .twoslash-completion-list {
user-select: none;
position: absolute;
top: 0;
left: 0;
transform: translate(0, 1.2em);
margin: 3px 0 0 -1px;
display: inline-block;
z-index: 8;
box-shadow: var(--twoslash-popup-shadow);
background: var(--twoslash-popup-bg);
border: 1px solid var(--twoslash-border-color);
}
.twoslash-completion-list {
width: 240px;
font-size: var(--twoslash-text-size);
padding: 4px;
display: flex;
flex-direction: column;
gap: 4px;
}
.twoslash-completion-list:hover {
user-select: auto;
}
.twoslash-completion-list::before {
background-color: var(--twoslash-cursor-color);
width: 2px;
position: absolute;
top: -1.6em;
height: 1.4em;
left: -1px;
content: ' ';
}
.twoslash-completion-list li {
overflow: hidden;
display: flex;
align-items: center;
gap: 0.25em;
line-height: 1em;
}
.twoslash-completion-list li span.twoslash-completions-unmatched {
color: var(--twoslash-unmatched-color);
}
.twoslash-completion-list .deprecated {
text-decoration: line-through;
opacity: 0.5;
}
.twoslash-completion-list li span.twoslash-completions-matched {
color: var(--twoslash-matched-color);
}
/* Highlights */
.twoslash-highlighted {
background-color: var(--twoslash-highlighted-bg);
border: 1px solid var(--twoslash-highlighted-border);
padding: 1px 2px;
margin: -1px -3px;
border-radius: 4px;
}
/* Icons */
.twoslash-completion-list .twoslash-completions-icon {
color: var(--twoslash-unmatched-color);
width: 1em;
flex: none;
}
/* Custom Tags */
.mint-twoslash-popover .twoslash-tag-line {
position: relative;
background-color: var(--twoslash-tag-bg);
border-left: 3px solid var(--twoslash-tag-color);
color: var(--twoslash-tag-color);
padding: 6px 10px;
margin: 0.2em 0;
display: flex;
align-items: center;
gap: 0.3em;
min-width: 100%;
width: max-content;
}
.mint-twoslash-popover .twoslash-tag-line .twoslash-tag-icon {
width: 1.1em;
color: inherit;
}
.mint-twoslash-popover .twoslash-tag-line.twoslash-tag-error-line {
background-color: var(--twoslash-error-bg);
border-left: 3px solid var(--twoslash-error-color);
color: var(--twoslash-error-color);
}
.mint-twoslash-popover .twoslash-tag-line.twoslash-tag-warn-line {
background-color: var(--twoslash-tag-warn-bg);
border-left: 3px solid var(--twoslash-tag-warn-color);
color: var(--twoslash-tag-warn-color);
}
.mint-twoslash-popover .twoslash-tag-line.twoslash-tag-annotate-line {
background-color: var(--twoslash-tag-annotate-bg);
border-left: 3px solid var(--twoslash-tag-annotate-color);
color: var(--twoslash-tag-annotate-color);
}
================================================
FILE: examples/app-router/app/layout.tsx
================================================
import type { Metadata } from 'next';
import '@/app/globals.css';
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
================================================
FILE: examples/app-router/app/loading.tsx
================================================
export default function Loading() {
return <>Loading...>;
}
================================================
FILE: examples/app-router/app/page.tsx
================================================
import { MDXRemote } from '@mintlify/mdx/rsc';
import { promises as fs } from 'fs';
export default async function Home() {
const data = await fs.readFile(process.cwd() + '/examples/highlight-example.mdx', 'utf8');
return (
);
}
================================================
FILE: examples/app-router/examples/highlight-example.mdx
================================================
---
title: 'Line Highlighting'
description: 'Highlights specific lines and/or line ranges'
---
This MDX file demonstrates syntax highlighting for various programming languages.
## JavaScript
```js index.js {2}
console.log('Hello, world!');
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
```
## Python
```python index.py {1-2,4-5}
def add(a, b):
return a + b
def subtract(a, b):
return a - b
```
## Java
```java {3}
public class Main {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
```
## C#
```csharp index.cs {1,3-4}
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
```
## Testing Twoslash
### Twoslash disabled without any additional configs or filenames
```ts
// This is a tooltip that will appear on the next line
const myVariable = 'hello world';
// ^?
// This is the second line
// You can include [links](#anchor) in your hover content
function myFunction() {
// ^?
return myVariable;
}
```
### Twoslash enabled without any additional configs or filenames
```ts twoslash
// This is a tooltip that will appear on the next line
const myVariable = 'hello world';
// ^?
// This is the second line
// You can include [links](#anchor) in your hover content
function myFunction() {
// ^?
return myVariable;
}
```
### Twoslash disabled with additional configs and filename
```js something_with_external_packages.tsx icon="js" lines
import { useEffect, useState } from 'react';
export function Component() {
// ^?
const [count, setCount] = useState(0);
// ^? ^?
useEffect(() => {
setTimeout(() => setCount(count + 1), 1000);
// ^?
}, [count]);
return {count}
;
}
```
### Twoslash enabled with additional configs
```js something_with_external_packages.tsx icon="js" lines twoslash
import { useEffect, useState } from 'react';
export function Component() {
// ^?
const [count, setCount] = useState(0);
// ^? ^?
useEffect(() => {
setTimeout(() => setCount(count + 1), 1000);
// ^?
}, [count]);
return {count}
;
}
```
## Link support
```js Link Testing icon="js" lines twoslash
import { useEffect, useState } from 'react';
// @link Component
export function Component() {
// ^?
return {count}
;
}
// @link OtherFunction: #hola-there
export function OtherFunction() {
// ^?
return {count}
;
}
// @link ExternalLink: https://google.com
export function ExternalLink() {
// ^?
const str =
"Don't worry, only hover targets with ExternalLink will be affected, not random strings";
return {count}
;
}
```
```ts twoslash
type PermissionResult =
| {
behavior: 'allow';
updatedInput: ToolInput;
updatedPermissions?: PermissionUpdate[];
}
| {
behavior: 'deny';
message: string;
interrupt?: boolean;
};
type ToolInput = string[];
type PermissionUpdate = {
name: string;
permission: Array;
};
// ---cut-before---
type CanUseTool = (
toolName: string,
input: ToolInput,
options: {
signal: AbortSignal;
suggestions?: PermissionUpdate[];
// ^?
}
) => Promise;
```
### Component
Hello world from the `Component` section
================================================
FILE: examples/app-router/next.config.js
================================================
const path = require('path');
/** @type {import('next').NextConfig} */
const nextConfig = {
serverExternalPackages: ['@shikijs/twoslash'],
outputFileTracingIncludes: {
'/render': [
path.relative(
process.cwd(),
path.resolve(require.resolve('typescript/package.json'), '..', 'lib', 'lib.*.d.ts')
),
'./node_modules/@types/node/**',
],
},
};
module.exports = nextConfig;
================================================
FILE: examples/app-router/package.json
================================================
{
"name": "app-router",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@mintlify/mdx": "workspace:^",
"@radix-ui/react-popover": "^1.1.15",
"next": "16.0.9",
"react": "^19.2.1",
"react-dom": "^19.2.1"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.10",
"@types/node": "^20",
"@types/react": "^19.2.1",
"@types/react-dom": "^19.2.1",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "16.0.7",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
}
================================================
FILE: examples/app-router/postcss.config.js
================================================
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
================================================
FILE: examples/app-router/readme.md
================================================
## Getting Started
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) and it uses the [App Router](https://nextjs.org/docs/app). It also uses [Tailwind CSS](https://tailwindcss.com/) for styling.
You can check out the code at [https://github.com/mintlify/mdx/blob/main/examples/app-router/app/page.tsx](https://github.com/mintlify/mdx/blob/main/examples/app-router/app/page.tsx) to understand how to parse your markdown using [@mintlify/mdx](https://www.npmjs.com/package/@mintlify/mdx).
## Demo
You can check out the demo of [this page](https://github.com/mintlify/mdx/blob/main/examples/app-router/app/page.tsx) at [https://mdx-app-router.vercel.app](https://mdx-app-router.vercel.app).
## How to use
1. Use the `MDXRemote` component directly inside your async React Server Component.
```tsx
import { MDXRemote } from '@mintlify/mdx/rsc';
export default async function Home() {
const source: `---
title: Title
---
## Markdown H2
`;
return (
);
}
```
================================================
FILE: examples/app-router/tailwind.config.ts
================================================
import type { Config } from 'tailwindcss';
const config: Config = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
},
},
},
plugins: [require('@tailwindcss/typography')],
};
export default config;
================================================
FILE: examples/app-router/tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
================================================
FILE: examples/pages-router/.eslintrc.json
================================================
{
"extends": "next/core-web-vitals"
}
================================================
FILE: examples/pages-router/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
================================================
FILE: examples/pages-router/eslint.config.mjs
================================================
import { defineConfig } from "eslint/config";
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default defineConfig([{
extends: [...nextCoreWebVitals],
}]);
================================================
FILE: examples/pages-router/examples/highlight-example.mdx
================================================
---
title: 'Line Highlighting'
description: 'Highlights specific lines and/or line ranges'
---
This MDX file demonstrates syntax highlighting for various programming languages.
## JavaScript
```js index.js {2}
console.log('Hello, world!');
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
```
## Python
```python index.py {1-2,4-5}
def add(a, b):
return a + b
def subtract(a, b):
return a - b
```
## Java
```java {3}
public class Main {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
```
## C#
```csharp index.cs {1,3-4}
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
```
## Testing Twoslash
### Twoslash disabled without any additional configs or filenames
```ts
// This is a tooltip that will appear on the next line
const myVariable = 'hello world';
// ^?
// This is the second line
// You can include [links](#anchor) in your hover content
function myFunction() {
// ^?
return myVariable;
}
```
### Twoslash enabled without any additional configs or filenames
```ts twoslash
// This is a tooltip that will appear on the next line
const myVariable = 'hello world';
// ^?
// This is the second line
// You can include [links](#anchor) in your hover content
function myFunction() {
// ^?
return myVariable;
}
```
### Twoslash disabled with additional configs and filename
```js something_with_external_packages.tsx icon="js" lines
import { useEffect, useState } from 'react';
export function Component() {
// ^?
const [count, setCount] = useState(0);
// ^? ^?
useEffect(() => {
setTimeout(() => setCount(count + 1), 1000);
// ^?
}, [count]);
return {count}
;
}
```
### Twoslash enabled with additional configs
```js something_with_external_packages.tsx icon="js" lines twoslash
import { useEffect, useState } from 'react';
export function Component() {
// ^?
const [count, setCount] = useState(0);
// ^? ^?
useEffect(() => {
setTimeout(() => setCount(count + 1), 1000);
// ^?
}, [count]);
return {count}
;
}
```
## Link support
```js Link Testing icon="js" lines twoslash
import { useEffect, useState } from 'react';
// @link Component
export function Component() {
// ^?
return {count}
;
}
// @link OtherFunction: #hola-there
export function OtherFunction() {
// ^?
return {count}
;
}
// @link ExternalLink: https://google.com
export function ExternalLink() {
// ^?
const str =
"Don't worry, only hover targets with ExternalLink will be affected, not random strings";
return {count}
;
}
```
```ts twoslash
type PermissionResult =
| {
behavior: 'allow';
updatedInput: ToolInput;
updatedPermissions?: PermissionUpdate[];
}
| {
behavior: 'deny';
message: string;
interrupt?: boolean;
};
type ToolInput = string[];
type PermissionUpdate = {
name: string;
permission: Array;
};
// ---cut-before---
type CanUseTool = (
toolName: string,
input: ToolInput,
options: {
signal: AbortSignal;
suggestions?: PermissionUpdate[];
// ^?
}
) => Promise;
```
### Component
Hello world from the `Component` section
================================================
FILE: examples/pages-router/next.config.js
================================================
const path = require('path');
/** @type {import('next').NextConfig} */
const nextConfig = {
serverExternalPackages: ['@shikijs/twoslash'],
outputFileTracingIncludes: {
'/render': [
path.relative(
process.cwd(),
path.resolve(require.resolve('typescript/package.json'), '..', 'lib', 'lib.*.d.ts')
),
'./node_modules/@types/node/**',
],
},
};
module.exports = nextConfig;
================================================
FILE: examples/pages-router/package.json
================================================
{
"name": "pages-router",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint ."
},
"dependencies": {
"@mintlify/mdx": "workspace:^",
"@radix-ui/react-popover": "^1.1.15",
"next": "16.0.9",
"react": "^19.2.1",
"react-dom": "^19.2.1"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.10",
"@types/node": "^20",
"@types/react": "^19.2.1",
"@types/react-dom": "^19.2.1",
"autoprefixer": "^10.0.1",
"eslint": "^9",
"eslint-config-next": "16.0.7",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"typescript": "^5"
},
"resolutions": {
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3"
}
}
================================================
FILE: examples/pages-router/pages/_app.tsx
================================================
import { AppProps } from 'next/app';
import '@/styles/globals.css';
export default function App({ Component, pageProps }: AppProps) {
return ;
}
================================================
FILE: examples/pages-router/pages/_document.tsx
================================================
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
);
}
================================================
FILE: examples/pages-router/pages/index.tsx
================================================
import { MDXClient } from '@mintlify/mdx/client';
import { serialize } from '@mintlify/mdx/server';
import type { SerializeResult } from '@mintlify/mdx/types';
import { promises as fs } from 'fs';
import type { GetStaticProps, InferGetStaticPropsType } from 'next';
export const getStaticProps = (async () => {
const data = await fs.readFile(process.cwd() + '/examples/highlight-example.mdx', 'utf8');
const mdxSource = await serialize({ source: data });
if ('error' in mdxSource) {
throw mdxSource.error;
}
return { props: { mdxSource } };
}) satisfies GetStaticProps<{
mdxSource: Omit;
}>;
export default function Home({ mdxSource }: InferGetStaticPropsType) {
return (
{String(mdxSource.frontmatter.title)}
);
}
================================================
FILE: examples/pages-router/postcss.config.js
================================================
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
================================================
FILE: examples/pages-router/readme.md
================================================
## Getting Started
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) and it uses the [Pages Router](https://nextjs.org/docs/pages). It also uses [Tailwind CSS](https://tailwindcss.com/) for styling.
You can check out the code at [https://github.com/mintlify/mdx/blob/main/examples/pages-router/pages/index.tsx](https://github.com/mintlify/mdx/blob/main/examples/pages-router/pages/index.tsx) to understand how to parse your markdown using [@mintlify/mdx](https://www.npmjs.com/package/@mintlify/mdx).
## Demo
You can check out the demo of [this page](https://github.com/mintlify/mdx/blob/main/examples/pages-router/pages/index.tsx) at [https://mdx-pages-router.vercel.app](https://mdx-pages-router.vercel.app).
## How to use
1. Call the `serialize` function inside `getStaticProps` and return the `mdxSource` object.
```tsx
export const getStaticProps = (async () => {
const mdxSource = await serialize({
source: '## Markdown H2',
});
if ('error' in mdxSource) {
// handle error case
}
return { props: { mdxSource } };
}) satisfies GetStaticProps<{
mdxSource: SerializeSuccess;
}>;
```
2. Pass the `mdxSource` object as props inside the `MDXComponent`.
```tsx
export default function Page({ mdxSource }: InferGetStaticPropsType) {
return ;
}
```
================================================
FILE: examples/pages-router/styles/globals.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--primary-light: 85 215 153;
}
}
/* modified from https://github.com/shikijs/shiki/blob/main/packages/twoslash/style-rich.css */
/* ===== Basic ===== */
:root {
--twoslash-border-color: #8888;
--twoslash-underline-color: currentColor;
--twoslash-highlighted-border: #c37d0d50;
--twoslash-highlighted-bg: #c37d0d20;
--twoslash-popup-bg: #f8f8f8;
--twoslash-popup-color: inherit;
--twoslash-popup-shadow: rgba(0, 0, 0, 0.08) 0px 1px 4px;
--twoslash-docs-color: #888;
--twoslash-docs-font: sans-serif;
--twoslash-code-font: inherit;
--twoslash-code-font-size: 1em;
--twoslash-matched-color: inherit;
--twoslash-unmatched-color: #888;
--twoslash-cursor-color: #8888;
--twoslash-error-color: #d45656;
--twoslash-error-bg: #d4565620;
--twoslash-warn-color: #c37d0d;
--twoslash-warn-bg: #c37d0d20;
--twoslash-tag-color: #3772cf;
--twoslash-tag-bg: #3772cf20;
--twoslash-tag-warn-color: var(--twoslash-warn-color);
--twoslash-tag-warn-bg: var(--twoslash-warn-bg);
--twoslash-tag-annotate-color: #1ba673;
--twoslash-tag-annotate-bg: #1ba67320;
--twoslash-text-size: 0.8rem;
--twoslash-docs-tag-style: italic;
}
/* Respect people's wishes to not have animations */
@media (prefers-reduced-motion: reduce) {
.twoslash * {
transition: none !important;
}
}
/* ===== Hover Info ===== */
.twoslash:hover .twoslash-hover {
border-color: var(--twoslash-underline-color);
}
.twoslash .twoslash-hover {
border-bottom: 1px dotted transparent;
transition-timing-function: ease;
transition: border-color 0.3s;
position: relative;
}
/* ===== Popup Override ===== */
.mint-twoslash-popover {
background: var(--twoslash-popup-bg);
color: var(--twoslash-popup-color);
border: 1px solid var(--twoslash-border-color);
border-radius: 4px;
pointer-events: auto;
text-align: left;
box-shadow: var(--twoslash-popup-shadow);
display: inline-flex;
flex-direction: column;
}
.mint-twoslash-popover-pre {
display: flex;
font-size: var(--twoslash-text-size);
font-family: var(--twoslash-code-font);
}
.mint-twoslash-popover:hover {
user-select: auto;
}
.twoslash .twoslash-popup-arrow {
display: none;
}
.twoslash-popup-code,
.twoslash-popup-error,
.twoslash-popup-docs {
padding: 6px 8px !important;
}
.mint-twoslash-popover .twoslash-popup-docs {
color: var(--twoslash-docs-color);
font-family: var(--twoslash-docs-font);
font-size: var(--twoslash-text-size);
max-width: unset;
}
.mint-twoslash-popover .twoslash-popup-error {
color: var(--twoslash-error-color);
background-color: var(--twoslash-error-bg);
font-family: var(--twoslash-docs-font);
font-size: var(--twoslash-text-size);
}
.mint-twoslash-popover .twoslash-popup-docs-tags {
display: flex;
flex-direction: column;
font-family: var(--twoslash-docs-font);
}
.mint-twoslash-popover .twoslash-popup-docs-tag-name {
margin-right: 0.5em;
font-style: var(--twoslash-docs-tag-style);
}
.mint-twoslash-popover .twoslash-popup-docs-tag-name {
font-family: var(--twoslash-code-font);
}
/* ===== Query Line ===== */
.mint-twoslash-popover .twoslash-query-line .twoslash-popup-container {
position: relative;
margin-bottom: 1.4em;
transform: translateY(0.6em);
}
/* ===== Error Line ===== */
.mint-twoslash-popover .twoslash-error-line {
position: relative;
background-color: var(--twoslash-error-bg);
border-left: 3px solid var(--twoslash-error-color);
color: var(--twoslash-error-color);
padding: 6px 12px;
margin: 0.2em 0;
min-width: 100%;
width: max-content;
}
.mint-twoslash-popover .twoslash-error-line.twoslash-error-level-warning {
background-color: var(--twoslash-warn-bg);
border-left: 3px solid var(--twoslash-warn-color);
color: var(--twoslash-warn-color);
}
.mint-twoslash-popover .twoslash-error {
background: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23c94824'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E") repeat-x bottom left;
padding-bottom: 2px;
}
.mint-twoslash-popover .twoslash-error.twoslash-error-level-warning {
background: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23c37d0d'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E") repeat-x bottom left;
padding-bottom: 2px;
}
/* ===== Completeions ===== */
.mint-twoslash-popover .twoslash-completion-cursor {
position: relative;
}
.mint-twoslash-popover .twoslash-completion-cursor .twoslash-completion-list {
user-select: none;
position: absolute;
top: 0;
left: 0;
transform: translate(0, 1.2em);
margin: 3px 0 0 -1px;
display: inline-block;
z-index: 8;
box-shadow: var(--twoslash-popup-shadow);
background: var(--twoslash-popup-bg);
border: 1px solid var(--twoslash-border-color);
}
.twoslash-completion-list {
width: 240px;
font-size: var(--twoslash-text-size);
padding: 4px;
display: flex;
flex-direction: column;
gap: 4px;
}
.twoslash-completion-list:hover {
user-select: auto;
}
.twoslash-completion-list::before {
background-color: var(--twoslash-cursor-color);
width: 2px;
position: absolute;
top: -1.6em;
height: 1.4em;
left: -1px;
content: ' ';
}
.twoslash-completion-list li {
overflow: hidden;
display: flex;
align-items: center;
gap: 0.25em;
line-height: 1em;
}
.twoslash-completion-list li span.twoslash-completions-unmatched {
color: var(--twoslash-unmatched-color);
}
.twoslash-completion-list .deprecated {
text-decoration: line-through;
opacity: 0.5;
}
.twoslash-completion-list li span.twoslash-completions-matched {
color: var(--twoslash-matched-color);
}
/* Highlights */
.twoslash-highlighted {
background-color: var(--twoslash-highlighted-bg);
border: 1px solid var(--twoslash-highlighted-border);
padding: 1px 2px;
margin: -1px -3px;
border-radius: 4px;
}
/* Icons */
.twoslash-completion-list .twoslash-completions-icon {
color: var(--twoslash-unmatched-color);
width: 1em;
flex: none;
}
/* Custom Tags */
.mint-twoslash-popover .twoslash-tag-line {
position: relative;
background-color: var(--twoslash-tag-bg);
border-left: 3px solid var(--twoslash-tag-color);
color: var(--twoslash-tag-color);
padding: 6px 10px;
margin: 0.2em 0;
display: flex;
align-items: center;
gap: 0.3em;
min-width: 100%;
width: max-content;
}
.mint-twoslash-popover .twoslash-tag-line .twoslash-tag-icon {
width: 1.1em;
color: inherit;
}
.mint-twoslash-popover .twoslash-tag-line.twoslash-tag-error-line {
background-color: var(--twoslash-error-bg);
border-left: 3px solid var(--twoslash-error-color);
color: var(--twoslash-error-color);
}
.mint-twoslash-popover .twoslash-tag-line.twoslash-tag-warn-line {
background-color: var(--twoslash-tag-warn-bg);
border-left: 3px solid var(--twoslash-tag-warn-color);
color: var(--twoslash-tag-warn-color);
}
.mint-twoslash-popover .twoslash-tag-line.twoslash-tag-annotate-line {
background-color: var(--twoslash-tag-annotate-bg);
border-left: 3px solid var(--twoslash-tag-annotate-color);
color: var(--twoslash-tag-annotate-color);
}
================================================
FILE: examples/pages-router/tailwind.config.ts
================================================
import type { Config } from 'tailwindcss';
const config: Config = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
},
},
},
plugins: [require('@tailwindcss/typography')],
};
export default config;
================================================
FILE: examples/pages-router/tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
================================================
FILE: package.json
================================================
{
"name": "mdx",
"private": true,
"scripts": {
"build": "yarn workspaces foreach --topological-dev -Av run build"
},
"workspaces": [
"packages/*",
"examples/*"
],
"packageManager": "yarn@4.3.1"
}
================================================
FILE: packages/mdx/package.json
================================================
{
"name": "@mintlify/mdx",
"version": "4.0.0",
"description": "Markdown parser from Mintlify",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"sideEffects": false,
"files": [
"dist"
],
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./rsc": {
"import": "./dist/client/rsc.js",
"types": "./dist/client/rsc.d.ts"
},
"./client": {
"import": "./dist/client/default.js",
"types": "./dist/client/default.d.ts"
},
"./server": {
"import": "./dist/server/index.js",
"types": "./dist/server/index.d.ts"
},
"./types": {
"import": "./dist/types/index.js",
"types": "./dist/types/index.d.ts"
},
"./plugins": {
"import": "./dist/plugins/index.js",
"types": "./dist/plugins/index.d.ts"
},
"./constants": {
"import": "./dist/plugins/rehype/shiki-constants.js",
"types": "./dist/plugins/rehype/shiki-constants.d.ts"
},
"./ui": {
"import": "./dist/ui/index.js",
"types": "./dist/ui/index.d.ts"
}
},
"type": "module",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"type": "git",
"url": "https://github.com/mintlify/mdx.git"
},
"scripts": {
"prepare": "npm run build",
"build": "tsc --project tsconfig.build.json",
"clean:build": "rimraf dist",
"clean:all": "rimraf node_modules .eslintcache && yarn clean:build",
"watch": "tsc --watch",
"type": "tsc --noEmit",
"lint": "eslint . --cache",
"format": "prettier . --write",
"format:check": "prettier . --check"
},
"author": "Mintlify, Inc.",
"license": "MIT",
"devDependencies": {
"@mintlify/eslint-config": "^1.0.4",
"@mintlify/eslint-config-typescript": "^1.0.9",
"@mintlify/prettier-config": "^1.0.1",
"@mintlify/ts-config": "^2.0.2",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@tsconfig/recommended": "1.x",
"@types/hast": "^3.0.4",
"@types/react": "^19.2.1",
"@types/react-dom": "^19.2.1",
"@types/unist": "^3.0.3",
"@typescript-eslint/eslint-plugin": "6.x",
"@typescript-eslint/parser": "6.x",
"eslint": "8.x",
"eslint-config-prettier": "8.x",
"eslint-plugin-unused-imports": "^3.x",
"prettier": "^3.1.1",
"prettier-plugin-tailwindcss": "^0.6.8",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"rimraf": "^5.0.1",
"typescript": "^5.7.2"
},
"peerDependencies": {
"@radix-ui/react-popover": "^19.2.1",
"react": "^19.2.1",
"react-dom": "^19.2.1"
},
"dependencies": {
"@shikijs/transformers": "^3.11.0",
"@shikijs/twoslash": "^3.12.2",
"arktype": "^2.1.26",
"hast-util-to-string": "^3.0.1",
"mdast-util-from-markdown": "^2.0.2",
"mdast-util-gfm": "^3.1.0",
"mdast-util-mdx-jsx": "^3.2.0",
"mdast-util-to-hast": "^13.2.0",
"next-mdx-remote-client": "^1.0.3",
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"remark-smartypants": "^3.0.2",
"shiki": "^3.11.0",
"unified": "^11.0.0",
"unist-util-visit": "^5.0.0"
}
}
================================================
FILE: packages/mdx/src/client/default.tsx
================================================
import { MDXClient as BaseMDXClient, MDXClientProps } from 'next-mdx-remote-client/csr';
import { Popup, PopupContent, PopupTrigger } from '../ui/index.js';
export function MDXClient(props: MDXClientProps) {
const mergedComponents = {
Popup,
PopupContent,
PopupTrigger,
...props.components,
};
return ;
}
================================================
FILE: packages/mdx/src/client/rsc.tsx
================================================
import { MDXRemote as BaseMDXRemote, MDXComponents } from 'next-mdx-remote-client/rsc';
import { SerializeOptions } from 'next-mdx-remote-client/serialize';
import rehypeKatex from 'rehype-katex';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import remarkSmartypants from 'remark-smartypants';
import { rehypeSyntaxHighlighting, RehypeSyntaxHighlightingOptions } from '../plugins/index.js';
import { Popup, PopupContent, PopupTrigger } from '../ui/index.js';
export async function MDXRemote({
source,
mdxOptions,
scope,
components,
parseFrontmatter,
syntaxHighlightingOptions,
}: {
source: string;
mdxOptions?: SerializeOptions['mdxOptions'];
scope?: SerializeOptions['scope'];
components?: MDXComponents;
parseFrontmatter?: SerializeOptions['parseFrontmatter'];
syntaxHighlightingOptions?: RehypeSyntaxHighlightingOptions;
}) {
const mergedComponents = {
Popup,
PopupContent,
PopupTrigger,
...components,
};
return (
);
}
================================================
FILE: packages/mdx/src/index.ts
================================================
================================================
FILE: packages/mdx/src/plugins/index.ts
================================================
export * from './rehype/index.js';
================================================
FILE: packages/mdx/src/plugins/rehype/index.ts
================================================
export * from './rehypeSyntaxHighlighting.js';
================================================
FILE: packages/mdx/src/plugins/rehype/rehypeSyntaxHighlighting.ts
================================================
import { transformerTwoslash } from '@shikijs/twoslash';
import { type } from 'arktype';
import type { Element, Root } from 'hast';
import { toString } from 'hast-util-to-string';
import type { MdxJsxFlowElementHast, MdxJsxTextElementHast } from 'mdast-util-mdx-jsx';
import { createHighlighter, type Highlighter } from 'shiki';
import type { Plugin } from 'unified';
import { visit } from 'unist-util-visit';
import {
type ShikiLang,
type ShikiTheme,
shikiColorReplacements,
DEFAULT_LANG_ALIASES,
DEFAULT_LANG,
DEFAULT_DARK_THEME,
DEFAULT_LIGHT_THEME,
DEFAULT_THEMES,
DEFAULT_LANGS,
SHIKI_TRANSFORMERS,
UNIQUE_LANGS,
} from './shiki-constants.js';
import { TextMateGrammar, TextMateGrammarType } from './shiki/custom-language.js';
import { getTwoslashOptions, parseLineComment } from './twoslash/config.js';
import { getLanguage } from './utils.js';
export type RehypeSyntaxHighlightingOptions = {
theme?: ShikiTheme;
themes?: Record<'light' | 'dark', ShikiTheme>;
codeStyling?: 'dark' | 'system' | 'light' | Record | null;
linkMap?: Map;
customLanguages?: string[];
};
let highlighterPromise: Promise | null = null;
async function getHighlighter(): Promise {
if (!highlighterPromise) {
highlighterPromise = createHighlighter({
themes: DEFAULT_THEMES,
langs: DEFAULT_LANGS,
});
}
return highlighterPromise;
}
export const rehypeSyntaxHighlighting: Plugin<[RehypeSyntaxHighlightingOptions?], Root, Root> = (
options = {}
) => {
return async (tree) => {
const nodesToProcess: Promise[] = [];
const customLanguageNames: string[] = [];
const themesToLoad: ShikiTheme[] = [];
if (options.themes) {
themesToLoad.push(options.themes.dark);
themesToLoad.push(options.themes.light);
} else if (options.theme) {
themesToLoad.push(options.theme);
}
const highlighter = await getHighlighter();
await Promise.all([
...themesToLoad
.filter(
(theme): theme is Exclude =>
!DEFAULT_THEMES.includes(theme) && theme !== 'css-variables'
)
.map((theme) => highlighter.loadTheme(theme)),
...(options.customLanguages?.map(async (unparsedLang) => {
const parsedLang = JSON.parse(unparsedLang);
const lang = TextMateGrammar(parsedLang);
if (lang instanceof type.errors) {
console.error(lang.summary);
return;
}
await highlighter.loadLanguage(lang);
const possibleNames = [lang.name, lang.displayName, ...(lang.aliases ?? [])];
customLanguageNames.push(...possibleNames.filter((l) => l != undefined));
}) ?? []),
]);
visit(tree, 'element', (node, index, parent) => {
const child = node.children[0];
if (
!parent ||
index === undefined ||
node.type !== 'element' ||
node.tagName !== 'pre' ||
!child ||
child.type !== 'element' ||
child.tagName !== 'code'
) {
return;
}
// set the metadata of `node` (which is a pre element) to that of
// `child` (which is the code element that likely contains all the metadata)
if (!Object.keys(node.properties).length) {
node.properties = child.properties;
}
if (!node.data) {
node.data = child.data;
}
let lang =
getLanguage(node, DEFAULT_LANG_ALIASES) ??
getLanguage(child, DEFAULT_LANG_ALIASES) ??
DEFAULT_LANG;
if (
!DEFAULT_LANGS.includes(lang) &&
!customLanguageNames.includes(lang) &&
UNIQUE_LANGS.includes(lang)
) {
nodesToProcess.push(
highlighter.loadLanguage(lang).then(() => {
traverseNode({ node, index, parent, highlighter, lang, options });
})
);
} else {
if (!UNIQUE_LANGS.includes(lang) && !customLanguageNames.includes(lang)) {
lang = DEFAULT_LANG;
}
traverseNode({ node, index, parent, highlighter, lang, options });
}
});
await Promise.all(nodesToProcess);
};
};
function traverseNode({
node,
index,
parent,
highlighter,
lang,
options,
}: {
node: Element;
index: number;
parent: Element | Root | MdxJsxTextElementHast | MdxJsxFlowElementHast;
highlighter: Highlighter;
lang: ShikiLang;
options: RehypeSyntaxHighlightingOptions;
}) {
try {
let code = toString(node);
const meta = node.data?.meta?.split(' ') ?? [];
const twoslashIndex = meta.findIndex((str) => str.toLowerCase() === 'twoslash');
const shouldUseTwoslash = twoslashIndex > -1;
if (node.data && node.data.meta && shouldUseTwoslash) {
meta.splice(twoslashIndex, 1);
node.data.meta = meta.join(' ').trim() || undefined;
}
const linkMap = options.linkMap ?? new Map();
if (shouldUseTwoslash) {
const splitCode = code.split('\n');
for (const [i, line] of splitCode.entries()) {
const parsedLineComment = parseLineComment(line);
if (!parsedLineComment) continue;
const { word, href } = parsedLineComment;
linkMap.set(word, href);
splitCode.splice(i, 1);
}
code = splitCode.join('\n');
}
const twoslashOptions = getTwoslashOptions({ linkMap });
const hast = highlighter.codeToHast(code, {
lang: lang ?? DEFAULT_LANG,
meta: shouldUseTwoslash ? { __raw: 'twoslash' } : undefined,
themes: {
light:
options.themes?.light ??
options.theme ??
(options.codeStyling === 'dark' ? DEFAULT_DARK_THEME : DEFAULT_LIGHT_THEME),
dark: options.themes?.dark ?? options.theme ?? DEFAULT_DARK_THEME,
},
colorReplacements: shikiColorReplacements,
tabindex: false,
tokenizeMaxLineLength: 1000,
transformers: [...SHIKI_TRANSFORMERS, transformerTwoslash(twoslashOptions)],
});
const codeElement = hast.children[0] as Element;
if (!codeElement) return;
const preChild = codeElement.children[0] as Element;
node.data = node.data ?? {};
codeElement.data = node.data;
codeElement.properties.language = lang;
if (preChild) {
preChild.data = node.data;
preChild.properties.language = lang;
}
parent.children.splice(index, 1, codeElement);
} catch (err) {
if (err instanceof Error && /Unknown language/.test(err.message)) {
return;
}
throw err;
}
}
export { TextMateGrammar, type TextMateGrammarType };
================================================
FILE: packages/mdx/src/plugins/rehype/shiki/custom-language.ts
================================================
import { scope } from 'arktype';
// Types come from the LanguageRegistration type in Shiki: node_modules/@shikijs/types/dist/index.d.ts
const types = scope({
ScopeName: 'string',
ScopePath: 'string',
ScopePattern: 'string',
IncludeString: 'string',
RegExpString: 'string | RegExp',
ILocation: {
filename: 'string',
line: 'number',
char: 'number',
},
ILocatable: {
'$vscodeTextmateLocation?': 'ILocation',
},
IRawCapturesMap: {
'[string]': 'IRawRule',
},
IRawRepositoryMap: {
'[string]': 'IRawRule',
},
IRawCaptures: 'IRawCapturesMap & ILocatable',
_IRawRule: {
'include?': 'IncludeString',
'name?': 'ScopeName',
'contentName?': 'ScopeName',
'match?': 'RegExpString',
'captures?': 'IRawCaptures',
'begin?': 'RegExpString',
'beginCaptures?': 'IRawCaptures',
'end?': 'RegExpString',
'endCaptures?': 'IRawCaptures',
'while?': 'RegExpString',
'whileCaptures?': 'IRawCaptures',
'patterns?': 'IRawRule[]',
'repository?': 'IRawRepository',
'applyEndPatternLast?': 'boolean',
'[string]': 'unknown',
},
IRawRule: '_IRawRule & ILocatable',
IRawRepository: 'IRawRepositoryMap & ILocatable',
_IRawGrammar: {
repository: 'IRawRepository',
scopeName: 'ScopeName',
patterns: 'IRawRule[]',
'injections?': {
'[string]': 'IRawRule',
},
'injectionSelector?': 'string',
'fileTypes?': 'string[]',
'name?': 'string',
'firstLineMatch?': 'string',
'[string]': 'unknown',
},
IRawGrammar: 'ILocatable & _IRawGrammar',
LanguageRegistration: {
name: 'string',
scopeName: 'string',
'displayName?': 'string',
'aliases?': 'string[]',
'embeddedLangs?': 'string[]',
'embeddedLangsLazy?': 'string[]',
'balancedBracketSelectors?': 'string[]',
'unbalancedBracketSelectors?': 'string[]',
'foldingStopMarker?': 'string',
'foldingStartMarker?': 'string',
'injectTo?': 'string[]',
'[string]': 'unknown',
},
TextMateGrammar: 'LanguageRegistration & IRawGrammar',
}).export();
export const TextMateGrammar = types.TextMateGrammar;
export type TextMateGrammarType = typeof TextMateGrammar.infer;
================================================
FILE: packages/mdx/src/plugins/rehype/shiki-constants.ts
================================================
import {
transformerNotationHighlight,
transformerNotationFocus,
transformerMetaHighlight,
transformerNotationDiff,
} from '@shikijs/transformers';
import type { ShikiTransformer } from '@shikijs/types';
import { createCssVariablesTheme } from 'shiki/core';
import type { BundledLanguage, ThemeRegistration } from 'shiki/types';
export const LINE_HIGHLIGHT_CLASS_NAME = 'line-highlight';
export const LINE_FOCUS_CLASS_NAME = 'line-focus';
export const LINE_DIFF_ADD_CLASS_NAME = 'line-diff line-add';
export const LINE_DIFF_REMOVE_CLASS_NAME = 'line-diff line-remove';
export type ShikiLang = BundledLanguage | 'ansi' | 'text';
export type ShikiTheme = (typeof SHIKI_THEMES)[number];
export const SHIKI_CSS_THEME = createCssVariablesTheme({
name: 'css-variables',
variablePrefix: '--mint-',
variableDefaults: {
'color-text': '#171717',
'color-background': 'transparent',
'token-constant': '#171717',
'token-string': '#297a3a',
'token-comment': '#666666',
'token-keyword': '#bd2864',
'token-parameter': '#a35200',
'token-function': '#0068d6',
'token-string-expression': '#297a3a',
'token-punctuation': '#171717',
'token-link': '#297a3a',
'ansi-black': '#000000',
'ansi-black-dim': '#00000080',
'ansi-red': '#bb0000',
'ansi-red-dim': '#bb000080',
'ansi-green': '#00bb00',
'ansi-green-dim': '#00bb0080',
'ansi-yellow': '#bbbb00',
'ansi-yellow-dim': '#bbbb0080',
'ansi-blue': '#0000bb',
'ansi-blue-dim': '#0000bb80',
'ansi-magenta': '#ff00ff',
'ansi-magenta-dim': '#ff00ff80',
'ansi-cyan': '#00bbbb',
'ansi-cyan-dim': '#00bbbb80',
'ansi-white': '#eeeeee',
'ansi-white-dim': '#eeeeee80',
'ansi-bright-black': '#555555',
'ansi-bright-black-dim': '#55555580',
'ansi-bright-red': '#ff5555',
'ansi-bright-red-dim': '#ff555580',
'ansi-bright-green': '#00ff00',
'ansi-bright-green-dim': '#00ff0080',
'ansi-bright-yellow': '#ffff55',
'ansi-bright-yellow-dim': '#ffff5580',
'ansi-bright-blue': '#5555ff',
'ansi-bright-blue-dim': '#5555ff80',
'ansi-bright-magenta': '#ff55ff',
'ansi-bright-magenta-dim': '#ff55ff80',
'ansi-bright-cyan': '#55ffff',
'ansi-bright-cyan-dim': '#55ffff80',
'ansi-bright-white': '#ffffff',
'ansi-bright-white-dim': '#ffffff80',
},
fontStyle: true,
});
export const DEFAULT_LANG = 'text' as const;
export const DEFAULT_DARK_THEME: ShikiTheme = 'dark-plus' as const;
export const DEFAULT_LIGHT_THEME: ShikiTheme = 'github-light-default' as const;
export const DEFAULT_THEMES: [ShikiTheme, ShikiTheme, ThemeRegistration] = [
DEFAULT_LIGHT_THEME,
DEFAULT_DARK_THEME,
SHIKI_CSS_THEME,
] as const;
export const shikiColorReplacements: Partial>> =
{
'dark-plus': {
'#1e1e1e': '#0B0C0E',
},
};
export const DEFAULT_LANG_ALIASES: Record = {
ansi: 'ansi',
abap: 'abap',
'actionscript-3': 'actionscript-3',
ada: 'ada',
'angular-html': 'angular-html',
'angular-ts': 'angular-ts',
apache: 'apache',
apex: 'apex',
apl: 'apl',
applescript: 'applescript',
ara: 'ara',
asciidoc: 'asciidoc',
adoc: 'asciidoc',
asm: 'asm',
astro: 'astro',
awk: 'awk',
ballerina: 'ballerina',
bat: 'bat',
batch: 'bat',
beancount: 'beancount',
berry: 'berry',
be: 'berry',
bibtex: 'bibtex',
bicep: 'bicep',
blade: 'blade',
bsl: 'bsl',
'1c': 'bsl',
c: 'c',
h: 'c',
cadence: 'cadence',
cdc: 'cadence',
cairo: 'cairo',
clarity: 'clarity',
clojure: 'clojure',
clj: 'clojure',
cmake: 'cmake',
cobol: 'cobol',
codeowners: 'codeowners',
codeql: 'codeql',
ql: 'codeql',
coffee: 'coffee',
coffeescript: 'coffee',
'common-lisp': 'common-lisp',
lisp: 'common-lisp',
coq: 'coq',
cpp: 'cpp',
cc: 'cpp',
hh: 'cpp',
'c++': 'cpp',
crystal: 'crystal',
csharp: 'csharp',
'c#': 'csharp',
cs: 'csharp',
css: 'css',
csv: 'csv',
cue: 'cue',
cypher: 'cypher',
cql: 'cypher',
d: 'd',
dart: 'dart',
dax: 'dax',
desktop: 'desktop',
diff: 'diff',
docker: 'docker',
dockerfile: 'docker',
dotenv: 'dotenv',
'dream-maker': 'dream-maker',
edge: 'edge',
elixir: 'elixir',
elm: 'elm',
'emacs-lisp': 'emacs-lisp',
elisp: 'emacs-lisp',
erb: 'erb',
erlang: 'erlang',
erl: 'erlang',
fennel: 'fennel',
fish: 'fish',
fluent: 'fluent',
ftl: 'fluent',
'fortran-fixed-form': 'fortran-fixed-form',
f: 'fortran-fixed-form',
for: 'fortran-fixed-form',
f77: 'fortran-fixed-form',
'fortran-free-form': 'fortran-free-form',
f90: 'fortran-free-form',
f95: 'fortran-free-form',
f03: 'fortran-free-form',
f08: 'fortran-free-form',
f18: 'fortran-free-form',
fsharp: 'fsharp',
'f#': 'fsharp',
fs: 'fsharp',
gdresource: 'gdresource',
gdscript: 'gdscript',
gdshader: 'gdshader',
genie: 'genie',
gherkin: 'gherkin',
'git-commit': 'git-commit',
'git-rebase': 'git-rebase',
gleam: 'gleam',
'glimmer-js': 'glimmer-js',
gjs: 'glimmer-js',
'glimmer-ts': 'glimmer-ts',
gts: 'glimmer-ts',
glsl: 'glsl',
gnuplot: 'gnuplot',
go: 'go',
graphql: 'graphql',
gql: 'graphql',
groovy: 'groovy',
hack: 'hack',
haml: 'haml',
handlebars: 'handlebars',
hbs: 'handlebars',
haskell: 'haskell',
hs: 'haskell',
haxe: 'haxe',
hcl: 'hcl',
hjson: 'hjson',
hlsl: 'hlsl',
html: 'html',
'html-derivative': 'html-derivative',
http: 'http',
hxml: 'hxml',
hy: 'hy',
imba: 'imba',
ini: 'ini',
properties: 'ini',
java: 'java',
javascript: 'javascript',
js: 'javascript',
jinja: 'jinja',
jison: 'jison',
json: 'json',
json5: 'json5',
jsonc: 'jsonc',
jsonl: 'jsonl',
jsonnet: 'jsonnet',
jssm: 'jssm',
fsl: 'jssm',
jsx: 'jsx',
julia: 'julia',
jl: 'julia',
kotlin: 'kotlin',
kt: 'kotlin',
kts: 'kotlin',
kusto: 'kusto',
kql: 'kusto',
latex: 'latex',
lean: 'lean',
lean4: 'lean',
less: 'less',
liquid: 'liquid',
llvm: 'llvm',
log: 'log',
logo: 'logo',
lua: 'lua',
luau: 'luau',
make: 'make',
makefile: 'make',
markdown: 'markdown',
md: 'markdown',
marko: 'marko',
matlab: 'matlab',
mdc: 'mdc',
mdx: 'mdx',
mermaid: 'mermaid',
mmd: 'mermaid',
mipsasm: 'mipsasm',
mips: 'mipsasm',
mojo: 'mojo',
move: 'move',
narrat: 'narrat',
nar: 'narrat',
nextflow: 'nextflow',
nf: 'nextflow',
nginx: 'nginx',
nim: 'nim',
nix: 'nix',
nushell: 'nushell',
nu: 'nushell',
'objective-c': 'objective-c',
objc: 'objective-c',
'objective-cpp': 'objective-cpp',
ocaml: 'ocaml',
pascal: 'pascal',
perl: 'perl',
php: 'php',
plsql: 'plsql',
po: 'po',
pot: 'po',
potx: 'po',
polar: 'polar',
postcss: 'postcss',
powerquery: 'powerquery',
powershell: 'powershell',
ps: 'powershell',
ps1: 'powershell',
prisma: 'prisma',
prolog: 'prolog',
proto: 'proto',
protobuf: 'proto',
pug: 'pug',
jade: 'pug',
puppet: 'puppet',
purescript: 'purescript',
python: 'python',
py: 'python',
qml: 'qml',
qmldir: 'qmldir',
qss: 'qss',
r: 'r',
racket: 'racket',
raku: 'raku',
perl6: 'raku',
razor: 'razor',
reg: 'reg',
regexp: 'regexp',
regex: 'regexp',
rel: 'rel',
riscv: 'riscv',
rst: 'rst',
ruby: 'ruby',
rb: 'ruby',
rust: 'rust',
rs: 'rust',
sas: 'sas',
sass: 'sass',
scala: 'scala',
scheme: 'scheme',
scss: 'scss',
sdbl: 'sdbl',
'1c-query': 'sdbl',
shaderlab: 'shaderlab',
shader: 'shaderlab',
shellscript: 'shellscript',
bash: 'shellscript',
sh: 'shellscript',
shell: 'shellscript',
zsh: 'shellscript',
shellsession: 'shellsession',
console: 'shellsession',
smalltalk: 'smalltalk',
solidity: 'solidity',
soy: 'soy',
'closure-templates': 'soy',
sparql: 'sparql',
splunk: 'splunk',
spl: 'splunk',
sql: 'sql',
'ssh-config': 'ssh-config',
stata: 'stata',
stylus: 'stylus',
styl: 'stylus',
svelte: 'svelte',
swift: 'swift',
'system-verilog': 'system-verilog',
systemd: 'systemd',
talonscript: 'talonscript',
talon: 'talonscript',
tasl: 'tasl',
tcl: 'tcl',
templ: 'templ',
terraform: 'terraform',
tf: 'terraform',
tfvars: 'terraform',
tex: 'tex',
toml: 'toml',
'ts-tags': 'ts-tags',
lit: 'ts-tags',
tsv: 'tsv',
tsx: 'tsx',
turtle: 'turtle',
twig: 'twig',
typescript: 'typescript',
ts: 'typescript',
typespec: 'typespec',
tsp: 'typespec',
typst: 'typst',
typ: 'typst',
txt: 'text',
text: 'text',
plaintext: 'text',
plain: 'text',
v: 'v',
vala: 'vala',
vb: 'vb',
cmd: 'vb',
verilog: 'verilog',
vhdl: 'vhdl',
viml: 'viml',
vim: 'viml',
vimscript: 'viml',
vue: 'vue',
'vue-html': 'vue-html',
vyper: 'vyper',
vy: 'vyper',
wasm: 'wasm',
wenyan: 'wenyan',
文言: 'wenyan',
wgsl: 'wgsl',
wikitext: 'wikitext',
mediawiki: 'wikitext',
wiki: 'wikitext',
wit: 'wit',
wolfram: 'wolfram',
wl: 'wolfram',
xml: 'xml',
xsl: 'xsl',
yaml: 'yaml',
yml: 'yaml',
zenscript: 'zenscript',
zig: 'zig',
};
export const UNIQUE_LANGS = Array.from(new Set(Object.values(DEFAULT_LANG_ALIASES)));
export const SHIKI_THEMES = [
'andromeeda',
'aurora-x',
'ayu-dark',
'catppuccin-frappe',
'catppuccin-latte',
'catppuccin-macchiato',
'catppuccin-mocha',
'dark-plus',
'dracula',
'dracula-soft',
'everforest-dark',
'everforest-light',
'github-dark',
'github-dark-default',
'github-dark-dimmed',
'github-dark-high-contrast',
'github-light',
'github-light-default',
'github-light-high-contrast',
'gruvbox-dark-hard',
'gruvbox-dark-medium',
'gruvbox-dark-soft',
'gruvbox-light-hard',
'gruvbox-light-medium',
'gruvbox-light-soft',
'houston',
'kanagawa-dragon',
'kanagawa-lotus',
'kanagawa-wave',
'laserwave',
'light-plus',
'material-theme',
'material-theme-darker',
'material-theme-lighter',
'material-theme-ocean',
'material-theme-palenight',
'min-dark',
'min-light',
'monokai',
'night-owl',
'nord',
'one-dark-pro',
'one-light',
'plastic',
'poimandres',
'red',
'rose-pine',
'rose-pine-dawn',
'rose-pine-moon',
'slack-dark',
'slack-ochin',
'snazzy-light',
'solarized-dark',
'solarized-light',
'synthwave-84',
'tokyo-night',
'vesper',
'vitesse-black',
'vitesse-dark',
'vitesse-light',
'css-variables', // for users who want to use custom CSS to style their code blocks
] as const;
export const DEFAULT_LANGS = [
'bash',
'blade',
'c',
'css',
'c#',
'c++',
'dart',
'diff',
'go',
'html',
'java',
'javascript',
'jsx',
'json',
'kotlin',
'log',
'lua',
'markdown',
'mdx',
'php',
'powershell',
'python',
'ruby',
'rust',
'solidity',
'swift',
'toml',
'typescript',
'tsx',
'yaml',
];
export const matchAlgorithm = {
matchAlgorithm: 'v3',
} as const;
export const SHIKI_TRANSFORMERS: ShikiTransformer[] = [
transformerMetaHighlight({
className: LINE_HIGHLIGHT_CLASS_NAME,
}),
transformerNotationHighlight({
...matchAlgorithm,
classActiveLine: LINE_HIGHLIGHT_CLASS_NAME,
}),
transformerNotationFocus({
...matchAlgorithm,
classActiveLine: LINE_FOCUS_CLASS_NAME,
}),
transformerNotationDiff({
...matchAlgorithm,
classLineAdd: LINE_DIFF_ADD_CLASS_NAME,
classLineRemove: LINE_DIFF_REMOVE_CLASS_NAME,
}),
];
================================================
FILE: packages/mdx/src/plugins/rehype/twoslash/config.ts
================================================
import { rendererRich, type TransformerTwoslashOptions } from '@shikijs/twoslash';
import type { Element, ElementContent } from 'hast';
import type { Code } from 'mdast';
import { fromMarkdown } from 'mdast-util-from-markdown';
import { gfmFromMarkdown } from 'mdast-util-gfm';
import { defaultHandlers, toHast } from 'mdast-util-to-hast';
import type { ShikiTransformerContextCommon } from 'shiki/types';
import ts from 'typescript';
const twoslashCompilerOptions: ts.CompilerOptions = {
target: ts.ScriptTarget.ESNext,
lib: ['ESNext', 'DOM', 'esnext', 'dom', 'es2020'],
};
function onTwoslashError(err: unknown, code: string, lang: string) {
console.error(JSON.stringify({ err, code, lang }));
}
function onShikiError(err: unknown, code: string, lang: string) {
console.error(JSON.stringify({ err, code, lang }));
}
export function getTwoslashOptions(
{ linkMap }: { linkMap: Map } = { linkMap: new Map() }
): TransformerTwoslashOptions {
return {
onTwoslashError,
onShikiError,
// copied fuma's approach for custom popup
// https://github.com/fuma-nama/fumadocs/blob/dev/packages/twoslash/src/index.ts
renderer: rendererRich({
renderMarkdown,
renderMarkdownInline,
queryRendering: 'line',
hast: {
hoverToken: {
tagName: 'Popup',
children(input) {
for (const rootElement of input) {
if (!('children' in rootElement)) continue;
for (const [i, element] of rootElement.children.entries()) {
if (element.type !== 'text') continue;
const href = linkMap.get(element.value);
if (!href) continue;
const linkProperties = {
href,
...(checkIsExternalLink(href) && {
target: '_blank',
rel: 'noopener noreferrer',
}),
};
if (rootElement.type === 'element' && rootElement.tagName === 'PopupTrigger') {
rootElement.properties = { ...rootElement.properties, ...linkProperties };
} else {
const newElement: ElementContent = {
type: 'element',
tagName: 'a',
properties: linkProperties,
children: [{ type: 'text', value: element.value }],
};
input.splice(i, 1, newElement);
}
}
}
return input;
},
},
hoverPopup: {
tagName: 'PopupContent',
},
hoverCompose: ({ popup, token }) => [
popup,
{
type: 'element',
tagName: 'PopupTrigger',
properties: {},
children: [token],
},
],
popupDocs: {
class: 'prose-sm prose-gray dark:prose-dark twoslash-popup-docs',
},
popupTypes: {
tagName: 'span',
class: 'mint-twoslash-popover-pre',
children: (v) => {
if (v.length === 1 && v[0]?.type === 'element' && v[0]?.tagName === 'pre') return v;
return [
{
type: 'element',
tagName: 'code',
properties: {
class: 'twoslash-popup-code shiki',
},
children: v,
},
];
},
},
popupDocsTags: {
class: 'prose-sm prose-gray dark:prose-dark twoslash-popup-docs twoslash-popup-docs-tags',
},
nodesHighlight: {
class: 'highlighted-word twoslash-highlighted',
},
},
}),
langs: ['ts', 'typescript', 'js', 'javascript', 'tsx', 'jsx'],
explicitTrigger: true,
twoslashOptions: {
compilerOptions: twoslashCompilerOptions,
},
};
}
/** https://github.com/fuma-nama/fumadocs/blob/2862a10c2d78b52c0a3f479ad21b255cc0031fc9/packages/twoslash/src/index.ts#L121-L150 */
function renderMarkdown(this: ShikiTransformerContextCommon, md: string): ElementContent[] {
const mdast = fromMarkdown(
md.replace(/{@link (? [^}]*)}/g, '$1'), // replace jsdoc links
{ mdastExtensions: [gfmFromMarkdown()] }
);
return (
toHast(mdast, {
handlers: {
code: (state, node: Code) => {
if (node.lang) {
return this.codeToHast(node.value, {
...this.options,
transformers: [],
meta: {
__raw: node.meta ?? undefined,
},
lang: node.lang,
}).children[0] as Element;
}
return defaultHandlers.code(state, node);
},
},
}) as Element
).children;
}
/** https://github.com/fuma-nama/fumadocs/blob/2862a10c2d78b52c0a3f479ad21b255cc0031fc9/packages/twoslash/src/index.ts#L152-L168 */
function renderMarkdownInline(
this: ShikiTransformerContextCommon,
md: string,
context?: string
): ElementContent[] {
const text = context === 'tag:param' ? md.replace(/^(? [\w$-]+)/, '`$1` ') : md;
const children = renderMarkdown.call(this, text);
if (children.length === 1 && children[0]?.type === 'element' && children[0].tagName === 'p')
return children[0].children;
return children;
}
export function parseLineComment(line: string): { word: string; href: string } | undefined {
line = line.trim();
if (!line.startsWith('//')) return;
line = line.replace(/^[\/\s]+/, '').trim();
if (!line.startsWith('@link ') && !line.startsWith('@link:')) return;
line = line.replace('@link:', '@link ');
const parts = line.split('@link ')[1];
if (!parts) return;
const words = parts.split(' ').filter(Boolean);
if (words.length === 1 && words[0]) {
let word = words[0];
if (word.endsWith(':')) word = word.slice(0, -1);
const lowercaseWord = word.toLowerCase();
const href = word.startsWith('#') ? lowercaseWord : `#${encodeURIComponent(lowercaseWord)}`;
return { word, href };
} else if (words.length === 2 && words[0] && words[1]) {
let word = words[0];
if (word.endsWith(':')) word = word.slice(0, -1);
const href = words[1];
if (!href.startsWith('#') && !href.startsWith('https://')) return;
return { word, href };
}
return;
}
type Url = `https://${string}`;
function checkIsExternalLink(href: string | undefined): href is Url {
let isExternalLink = false;
try {
if (href && URL.canParse(href)) isExternalLink = true;
} catch {}
return isExternalLink;
}
================================================
FILE: packages/mdx/src/plugins/rehype/utils.ts
================================================
import type { Element } from 'hast';
import { type ShikiLang } from './shiki-constants.js';
export function classNameOrEmptyArray(element: Element): string[] {
const className = element.properties.className;
if (Array.isArray(className) && className.every((el) => typeof el === 'string')) return className;
return [];
}
export function getLanguage(
node: Element,
aliases: Record
): ShikiLang | undefined {
const className = classNameOrEmptyArray(node);
for (const classListItem of className) {
if (classListItem.startsWith('language-')) {
const lang = classListItem.slice(9).toLowerCase();
if (lang) return aliases[lang] ?? (lang as ShikiLang);
}
}
return undefined;
}
================================================
FILE: packages/mdx/src/server/index.ts
================================================
import { serialize as baseSerialize } from 'next-mdx-remote-client/serialize';
import rehypeKatex from 'rehype-katex';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import remarkSmartypants from 'remark-smartypants';
import { rehypeSyntaxHighlighting, RehypeSyntaxHighlightingOptions } from '../plugins/index.js';
import type { SerializeOptions } from '../types/index.js';
export const serialize = async ({
source,
mdxOptions,
scope,
parseFrontmatter = true,
syntaxHighlightingOptions,
}: {
source: string;
mdxOptions?: SerializeOptions['mdxOptions'];
scope?: SerializeOptions['scope'];
parseFrontmatter?: SerializeOptions['parseFrontmatter'];
syntaxHighlightingOptions?: RehypeSyntaxHighlightingOptions;
}) => {
try {
return await baseSerialize({
source,
options: {
mdxOptions: {
...mdxOptions,
remarkPlugins: [
remarkGfm,
remarkSmartypants,
remarkMath,
...(mdxOptions?.remarkPlugins || []),
],
rehypePlugins: [
rehypeKatex,
[rehypeSyntaxHighlighting, syntaxHighlightingOptions],
...(mdxOptions?.rehypePlugins || []),
],
format: mdxOptions?.format || 'mdx',
},
scope,
parseFrontmatter,
},
});
} catch (error) {
console.error(`Error occurred while serializing MDX: ${error}`);
throw error;
}
};
================================================
FILE: packages/mdx/src/types/index.ts
================================================
import type { SerializeOptions, SerializeResult } from 'next-mdx-remote-client/serialize';
type SerializeSuccess = SerializeResult & { compiledSource: string };
export type { SerializeOptions, SerializeResult, SerializeSuccess };
================================================
FILE: packages/mdx/src/ui/index.ts
================================================
export * from './popup.js';
================================================
FILE: packages/mdx/src/ui/popup.tsx
================================================
'use client';
// copied from fuma's approach for custom popup
// https://github.com/fuma-nama/fumadocs/blob/dev/packages/twoslash/src/ui/popup.tsx
import { Popover, PopoverContent, PopoverPortal, PopoverTrigger } from '@radix-ui/react-popover';
import {
type ComponentPropsWithoutRef,
type ComponentRef,
createContext,
forwardRef,
type ReactNode,
useContext,
useMemo,
useRef,
useState,
} from 'react';
interface PopupContextObject {
open: boolean;
setOpen: (open: boolean) => void;
handleOpen: (e: React.PointerEvent) => void;
handleClose: (e: React.PointerEvent) => void;
}
const PopupContext = createContext(undefined);
function Popup({ delay = 300, children }: { delay?: number; children: ReactNode }) {
const [open, setOpen] = useState(false);
const openTimeoutRef = useRef(undefined);
const closeTimeoutRef = useRef(undefined);
return (
({
open,
setOpen,
handleOpen(e) {
if (e.pointerType === 'touch') return;
if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current);
openTimeoutRef.current = window.setTimeout(() => {
setOpen(true);
}, delay);
},
handleClose(e) {
if (e.pointerType === 'touch') return;
if (openTimeoutRef.current) clearTimeout(openTimeoutRef.current);
closeTimeoutRef.current = window.setTimeout(() => {
setOpen(false);
}, delay);
},
}),
[delay, open]
)}
>
{children}
);
}
const PopupTrigger = forwardRef<
ComponentRef,
ComponentPropsWithoutRef & { href?: string; target?: string; rel?: string }
>(({ children, href, target, rel, ...props }, ref) => {
const ctx = useContext(PopupContext);
if (!ctx) throw new Error('Missing Popup Context');
let element;
if (href) {
element = (
{children}
);
} else {
element = {children} ;
}
return (
{element}
);
});
PopupTrigger.displayName = 'PopupTrigger';
const PopupContent = forwardRef<
ComponentRef,
ComponentPropsWithoutRef
>(({ className, side = 'bottom', align = 'center', sideOffset = 4, ...props }, ref) => {
const ctx = useContext(PopupContext);
if (!ctx) throw new Error('Missing Popup Context');
return (
{
e.preventDefault();
}}
onCloseAutoFocus={(e) => {
e.preventDefault();
}}
{...props}
/>
);
});
PopupContent.displayName = 'PopupContent';
export { Popup, PopupTrigger, PopupContent };
================================================
FILE: packages/mdx/tsconfig.build.json
================================================
{
"extends": "./tsconfig.json",
"include": ["src/**/*.ts", "src/**/*.tsx"]
}
================================================
FILE: packages/mdx/tsconfig.json
================================================
{
"extends": "@mintlify/ts-config",
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"jsx": "react-jsx",
"target": "ES2021",
"outDir": "dist",
"declaration": true,
"module": "Node16"
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "dist"]
}
================================================
FILE: readme.md
================================================
Mint
Open source docs builder that's beautiful, fast, and easy to work with.
 [](https://twitter.com/intent/tweet?url=&text=Check%20out%20%40mintlify)
# Mintlify's markdown parser
**@mintlify/mdx** is a thin layer on top of [next-mdx-remote-client](https://github.com/ipikuka/next-mdx-remote-client) that provides a better developer experience for Next.js users by adding support for syntax highlighting.
## Installation
```bash
# using npm
npm i @mintlify/mdx
# using yarn
yarn add @mintlify/mdx
# using pnpm
pnpm add @mintlify/mdx
```
## Examples
### Next.js pages router
[You can check the example app here](https://github.com/mintlify/mdx/tree/main/examples/pages-router).
1. Call the `serialize` function inside `getStaticProps` and return the `mdxSource` object.
```tsx
export const getStaticProps = (async () => {
const mdxSource = await serialize({
source: '## Markdown H2',
});
if ('error' in mdxSource) {
// handle error case
}
return { props: { mdxSource } };
}) satisfies GetStaticProps<{
mdxSource: SerializeSuccess;
}>;
```
2. Pass the `mdxSource` object as props inside the `MDXComponent`.
```tsx
export default function Page({ mdxSource }: InferGetStaticPropsType) {
return ;
}
```
### Next.js app router
[You can check the example app here](https://github.com/mintlify/mdx/tree/main/examples/app-router).
1. Use the `MDXRemote` component directly inside your async React Server Component.
```tsx
import { MDXRemote } from '@mintlify/mdx';
export default async function Home() {
const source: `---
title: Title
---
## Markdown H2
`;
return (
);
}
```
## APIs
Similar to [next-mdx-remote-client](https://github.com/ipikuka/next-mdx-remote-client), this package exports the following APIs:
- `serialize` - a function that compiles MDX source to SerializeResult.
- `MDXClient` - a component that renders SerializeSuccess on the client.
- `MDXRemote` - a component that both serializes and renders the source - should be used inside async React Server Component.
### serialize
```tsx
import { serialize } from '@mintlify/mdx';
const mdxSource = await serialize({
source: '## Markdown H2',
mdxOptions: {
remarkPlugins: [
// Remark plugins
],
rehypePlugins: [
// Rehype plugins
],
},
});
```
### MDXClient
```tsx
'use client';
import { MDXClient } from '@mintlify/mdx';
;
```
### MDXRemote
```tsx
import { MDXRemote } from '@mintlify/mdx';
;
```