├── .c8rc.json
├── .editorconfig
├── .eslintignore
├── .eslintrc.yaml
├── .github
└── workflows
│ └── ci.yaml
├── .gitignore
├── .prettierignore
├── .prettierrc.yaml
├── .remarkrc.yaml
├── LICENSE.md
├── README.md
├── fixtures
├── boolean
│ ├── expected-code.jsx
│ ├── expected-pre.jsx
│ └── input.md
├── jsx-expression
│ ├── expected-code.jsx
│ ├── expected-pre.jsx
│ └── input.md
├── jsx-spread
│ ├── expected-code.jsx
│ ├── expected-pre.jsx
│ └── input.md
├── no-lang
│ ├── expected-code.jsx
│ ├── expected-pre.jsx
│ └── input.md
├── no-meta
│ ├── expected-code.jsx
│ ├── expected-pre.jsx
│ └── input.md
└── string
│ ├── expected-code.jsx
│ ├── expected-pre.jsx
│ └── input.md
├── package-lock.json
├── package.json
├── src
├── rehype-mdx-code-props.ts
└── test.ts
└── tsconfig.json
/.c8rc.json:
--------------------------------------------------------------------------------
1 | {
2 | "100": true,
3 | "exclude": ["**/test*"],
4 | "reporter": ["html", "lcov", "text"]
5 | }
6 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 2
7 | indent_style = space
8 | insert_final_newline = true
9 | max_line_length = 100
10 | trim_trailing_whitespace = true
11 |
12 | [COMMIT_EDITMSG]
13 | max_line_length = 72
14 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | *.jsx
2 |
--------------------------------------------------------------------------------
/.eslintrc.yaml:
--------------------------------------------------------------------------------
1 | root: true
2 | extends:
3 | - remcohaszing
4 | rules:
5 | no-param-reassign: off
6 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches: [main]
7 | tags: ['*']
8 |
9 | jobs:
10 | eslint:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: actions/setup-node@v4
15 | with:
16 | node-version: 20
17 | - run: npm ci
18 | - run: npx eslint .
19 |
20 | pack:
21 | runs-on: ubuntu-latest
22 | steps:
23 | - uses: actions/checkout@v4
24 | - uses: actions/setup-node@v4
25 | with:
26 | node-version: 20
27 | - run: npm ci
28 | - run: npm pack
29 | - uses: actions/upload-artifact@v4
30 | with:
31 | name: package
32 | path: '*.tgz'
33 |
34 | prettier:
35 | runs-on: ubuntu-latest
36 | steps:
37 | - uses: actions/checkout@v4
38 | - uses: actions/setup-node@v4
39 | with:
40 | node-version: 20
41 | - run: npm ci
42 | - run: npx prettier --check .
43 |
44 | remark:
45 | runs-on: ubuntu-latest
46 | steps:
47 | - uses: actions/checkout@v4
48 | - uses: actions/setup-node@v4
49 | with:
50 | node-version: 20
51 | - run: npm ci
52 | - run: npx remark --frail .
53 |
54 | test:
55 | runs-on: ubuntu-latest
56 | strategy:
57 | matrix:
58 | node-version:
59 | - 16
60 | - 18
61 | - 20
62 | steps:
63 | - uses: actions/checkout@v4
64 | - uses: actions/setup-node@v4
65 | with:
66 | node-version: ${{ matrix.node-version }}
67 | - run: npm ci
68 | - run: npm test
69 | - uses: codecov/codecov-action@v3
70 | if: ${{ matrix.node-version == 20 }}
71 |
72 | release:
73 | runs-on: ubuntu-latest
74 | needs:
75 | - eslint
76 | - pack
77 | - prettier
78 | - remark
79 | - test
80 | if: startsWith(github.ref, 'refs/tags/')
81 | permissions:
82 | id-token: write
83 | steps:
84 | - uses: actions/setup-node@v4
85 | with:
86 | node-version: 20
87 | registry-url: https://registry.npmjs.org
88 | - uses: actions/download-artifact@v4
89 | with: { name: package }
90 | - run: npm publish *.tgz --provenance --access public
91 | env:
92 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
93 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage/
2 | dist/
3 | node_modules/
4 | *.log
5 | *.tgz
6 | *.js
7 | *.d.ts
8 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | *.js
2 |
--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
1 | proseWrap: always
2 | semi: false
3 | singleQuote: true
4 | trailingComma: none
5 |
--------------------------------------------------------------------------------
/.remarkrc.yaml:
--------------------------------------------------------------------------------
1 | plugins:
2 | - remark-preset-remcohaszing
3 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | Copyright © 2023 Remco Haszing
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6 | associated documentation files (the “Software”), to deal in the Software without restriction,
7 | including without limitation the rights to use, copy, modify, merge, publish, distribute,
8 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all copies or substantial
12 | portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
15 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
16 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
17 | OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # rehype-mdx-code-props
2 |
3 | [](https://github.com/remcohaszing/rehype-mdx-code-props/actions/workflows/ci.yaml)
4 | [](https://codecov.io/gh/remcohaszing/rehype-mdx-code-props)
5 | [](https://www.npmjs.com/package/rehype-mdx-code-props)
6 | [](https://www.npmjs.com/package/rehype-mdx-code-props)
7 |
8 | A [rehype](https://github.com/rehypejs/rehype) [MDX](https://mdxjs.com) plugin for interpreting
9 | markdown code meta as props.
10 |
11 | ## Table of Contents
12 |
13 | - [Installation](#installation)
14 | - [Usage](#usage)
15 | - [API](#api)
16 | - [`rehypeMdxCodeProps`](#rehypemdxcodeprops)
17 | - [Compatibility](#compatibility)
18 | - [License](#license)
19 |
20 | ## Installation
21 |
22 | ```sh
23 | npm install rehype-mdx-code-props
24 | ```
25 |
26 | ## Usage
27 |
28 | This plugin interprets markdown code block metadata as JSX props.
29 |
30 | For example, given a file named `example.mdx` with the following content:
31 |
32 | ````markdown
33 | ```js copy filename="awesome.js" onOpen={props.openDemo} {...props}
34 | console.log('Everything is awesome!')
35 | ```
36 | ````
37 |
38 | The following script:
39 |
40 | ```js
41 | import { readFile } from 'node:fs/promises'
42 |
43 | import { compile } from '@mdx-js/mdx'
44 | import rehypeMdxCodeProps from 'rehype-mdx-code-props'
45 |
46 | const { value } = await compile(await readFile('example.mdx'), {
47 | jsx: true,
48 | rehypePlugins: [rehypeMdxCodeProps]
49 | })
50 | console.log(value)
51 | ```
52 |
53 | Roughly yields:
54 |
55 | ```jsx
56 | export default function MDXContent(props) {
57 | return (
58 |
59 | {"console.log('Everything is awesome!');\n"}
60 |
61 | )
62 | }
63 | ```
64 |
65 | The `` element doesn’t support those custom props. Use custom
66 | [components](https://mdxjs.com/docs/using-mdx/#components) to give the props meaning.
67 |
68 | > **Note** This plugin transforms the [`hast`](https://github.com/syntax-tree/hast) (HTML) nodes
69 | > into JSX. After running this plugin, they can no longer be processed by other plugins. To combine
70 | > it with other plugins, such as syntax highlighting plugins, `rehype-mdx-code-props` must run last.
71 |
72 | ## API
73 |
74 | This package has a default export `rehypeMdxCodeProps`.
75 |
76 | ### `rehypeMdxCodeProps`
77 |
78 | An MDX rehype plugin for transforming markdown code meta into JSX props.
79 |
80 | #### Options
81 |
82 | - `elementAttributeNameCase` (`'html' | 'react'`): The casing to use for attribute names. This
83 | should match the `elementAttributeNameCase` value passed to MDX. (Default: `'react'`)
84 | - `tagName` (`'code' | 'pre'`): The tag name to add the attributes to. (Default: `'pre'`)
85 |
86 | ## Compatibility
87 |
88 | This plugin works with Node.js 16 or greater and MDX 3.
89 |
90 | ## License
91 |
92 | [MIT](LICENSE.md) © [Remco Haszing](https://github.com/remcohaszing)
93 |
--------------------------------------------------------------------------------
/fixtures/boolean/expected-code.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic*/
2 | /*@jsxImportSource react*/
3 | function _createMdxContent(props) {
4 | const _components = {
5 | code: 'code',
6 | pre: 'pre',
7 | ...props.components
8 | }
9 | return (
10 | <_components.pre>
11 | <_components.code className="language-js" copy>
12 | {"console.log('Hello World!')\n"}
13 |
14 |
15 | )
16 | }
17 | export default function MDXContent(props = {}) {
18 | const { wrapper: MDXLayout } = props.components || {}
19 | return MDXLayout ? (
20 |
21 | <_createMdxContent {...props} />
22 |
23 | ) : (
24 | _createMdxContent(props)
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/fixtures/boolean/expected-pre.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic*/
2 | /*@jsxImportSource react*/
3 | function _createMdxContent(props) {
4 | const _components = {
5 | code: 'code',
6 | pre: 'pre',
7 | ...props.components
8 | }
9 | return (
10 | <_components.pre copy>
11 | <_components.code className="language-js">{"console.log('Hello World!')\n"}
12 |
13 | )
14 | }
15 | export default function MDXContent(props = {}) {
16 | const { wrapper: MDXLayout } = props.components || {}
17 | return MDXLayout ? (
18 |
19 | <_createMdxContent {...props} />
20 |
21 | ) : (
22 | _createMdxContent(props)
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/fixtures/boolean/input.md:
--------------------------------------------------------------------------------
1 | ```js copy
2 | console.log('Hello World!')
3 | ```
4 |
--------------------------------------------------------------------------------
/fixtures/jsx-expression/expected-code.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic*/
2 | /*@jsxImportSource react*/
3 | function _createMdxContent(props) {
4 | const _components = {
5 | code: 'code',
6 | pre: 'pre',
7 | ...props.components
8 | }
9 | return (
10 | <_components.pre>
11 | <_components.code className="language-js" onClick={props.onClick}>
12 | {"console.log('Hello World!')\n"}
13 |
14 |
15 | )
16 | }
17 | export default function MDXContent(props = {}) {
18 | const { wrapper: MDXLayout } = props.components || {}
19 | return MDXLayout ? (
20 |
21 | <_createMdxContent {...props} />
22 |
23 | ) : (
24 | _createMdxContent(props)
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/fixtures/jsx-expression/expected-pre.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic*/
2 | /*@jsxImportSource react*/
3 | function _createMdxContent(props) {
4 | const _components = {
5 | code: 'code',
6 | pre: 'pre',
7 | ...props.components
8 | }
9 | return (
10 | <_components.pre onClick={props.onClick}>
11 | <_components.code className="language-js">{"console.log('Hello World!')\n"}
12 |
13 | )
14 | }
15 | export default function MDXContent(props = {}) {
16 | const { wrapper: MDXLayout } = props.components || {}
17 | return MDXLayout ? (
18 |
19 | <_createMdxContent {...props} />
20 |
21 | ) : (
22 | _createMdxContent(props)
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/fixtures/jsx-expression/input.md:
--------------------------------------------------------------------------------
1 | ```js onClick={props.onClick}
2 | console.log('Hello World!')
3 | ```
4 |
--------------------------------------------------------------------------------
/fixtures/jsx-spread/expected-code.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic*/
2 | /*@jsxImportSource react*/
3 | function _createMdxContent(props) {
4 | const _components = {
5 | code: 'code',
6 | pre: 'pre',
7 | ...props.components
8 | }
9 | return (
10 | <_components.pre>
11 | <_components.code className="language-js" {...props}>
12 | {"console.log('Hello World!')\n"}
13 |
14 |
15 | )
16 | }
17 | export default function MDXContent(props = {}) {
18 | const { wrapper: MDXLayout } = props.components || {}
19 | return MDXLayout ? (
20 |
21 | <_createMdxContent {...props} />
22 |
23 | ) : (
24 | _createMdxContent(props)
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/fixtures/jsx-spread/expected-pre.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic*/
2 | /*@jsxImportSource react*/
3 | function _createMdxContent(props) {
4 | const _components = {
5 | code: 'code',
6 | pre: 'pre',
7 | ...props.components
8 | }
9 | return (
10 | <_components.pre {...props}>
11 | <_components.code className="language-js">{"console.log('Hello World!')\n"}
12 |
13 | )
14 | }
15 | export default function MDXContent(props = {}) {
16 | const { wrapper: MDXLayout } = props.components || {}
17 | return MDXLayout ? (
18 |
19 | <_createMdxContent {...props} />
20 |
21 | ) : (
22 | _createMdxContent(props)
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/fixtures/jsx-spread/input.md:
--------------------------------------------------------------------------------
1 | ```js {...props}
2 | console.log('Hello World!')
3 | ```
4 |
--------------------------------------------------------------------------------
/fixtures/no-lang/expected-code.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic*/
2 | /*@jsxImportSource react*/
3 | function _createMdxContent(props) {
4 | const _components = {
5 | code: 'code',
6 | pre: 'pre',
7 | ...props.components
8 | }
9 | return (
10 | <_components.pre>
11 | <_components.code>{"console.log('Hello World!');\n"}
12 |
13 | )
14 | }
15 | export default function MDXContent(props = {}) {
16 | const { wrapper: MDXLayout } = props.components || {}
17 | return MDXLayout ? (
18 |
19 | <_createMdxContent {...props} />
20 |
21 | ) : (
22 | _createMdxContent(props)
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/fixtures/no-lang/expected-pre.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic*/
2 | /*@jsxImportSource react*/
3 | function _createMdxContent(props) {
4 | const _components = {
5 | code: 'code',
6 | pre: 'pre',
7 | ...props.components
8 | }
9 | return (
10 | <_components.pre>
11 | <_components.code>{"console.log('Hello World!');\n"}
12 |
13 | )
14 | }
15 | export default function MDXContent(props = {}) {
16 | const { wrapper: MDXLayout } = props.components || {}
17 | return MDXLayout ? (
18 |
19 | <_createMdxContent {...props} />
20 |
21 | ) : (
22 | _createMdxContent(props)
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/fixtures/no-lang/input.md:
--------------------------------------------------------------------------------
1 | ```
2 | console.log('Hello World!');
3 | ```
4 |
--------------------------------------------------------------------------------
/fixtures/no-meta/expected-code.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic*/
2 | /*@jsxImportSource react*/
3 | function _createMdxContent(props) {
4 | const _components = {
5 | code: 'code',
6 | pre: 'pre',
7 | ...props.components
8 | }
9 | return (
10 | <_components.pre>
11 | <_components.code className="language-js">{"console.log('Hello World!')\n"}
12 |
13 | )
14 | }
15 | export default function MDXContent(props = {}) {
16 | const { wrapper: MDXLayout } = props.components || {}
17 | return MDXLayout ? (
18 |
19 | <_createMdxContent {...props} />
20 |
21 | ) : (
22 | _createMdxContent(props)
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/fixtures/no-meta/expected-pre.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic*/
2 | /*@jsxImportSource react*/
3 | function _createMdxContent(props) {
4 | const _components = {
5 | code: 'code',
6 | pre: 'pre',
7 | ...props.components
8 | }
9 | return (
10 | <_components.pre>
11 | <_components.code className="language-js">{"console.log('Hello World!')\n"}
12 |
13 | )
14 | }
15 | export default function MDXContent(props = {}) {
16 | const { wrapper: MDXLayout } = props.components || {}
17 | return MDXLayout ? (
18 |
19 | <_createMdxContent {...props} />
20 |
21 | ) : (
22 | _createMdxContent(props)
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/fixtures/no-meta/input.md:
--------------------------------------------------------------------------------
1 | ```js
2 | console.log('Hello World!')
3 | ```
4 |
--------------------------------------------------------------------------------
/fixtures/string/expected-code.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic*/
2 | /*@jsxImportSource react*/
3 | function _createMdxContent(props) {
4 | const _components = {
5 | code: 'code',
6 | pre: 'pre',
7 | ...props.components
8 | }
9 | return (
10 | <_components.pre>
11 | <_components.code className="language-js" filename="script.js">
12 | {"console.log('Hello World!')\n"}
13 |
14 |
15 | )
16 | }
17 | export default function MDXContent(props = {}) {
18 | const { wrapper: MDXLayout } = props.components || {}
19 | return MDXLayout ? (
20 |
21 | <_createMdxContent {...props} />
22 |
23 | ) : (
24 | _createMdxContent(props)
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/fixtures/string/expected-pre.jsx:
--------------------------------------------------------------------------------
1 | /*@jsxRuntime automatic*/
2 | /*@jsxImportSource react*/
3 | function _createMdxContent(props) {
4 | const _components = {
5 | code: 'code',
6 | pre: 'pre',
7 | ...props.components
8 | }
9 | return (
10 | <_components.pre filename="script.js">
11 | <_components.code className="language-js">{"console.log('Hello World!')\n"}
12 |
13 | )
14 | }
15 | export default function MDXContent(props = {}) {
16 | const { wrapper: MDXLayout } = props.components || {}
17 | return MDXLayout ? (
18 |
19 | <_createMdxContent {...props} />
20 |
21 | ) : (
22 | _createMdxContent(props)
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/fixtures/string/input.md:
--------------------------------------------------------------------------------
1 | ```js filename="script.js"
2 | console.log('Hello World!')
3 | ```
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rehype-mdx-code-props",
3 | "version": "3.0.1",
4 | "description": "A rehype MDX plugin for interpreting markdown code meta as props",
5 | "author": "Remco Haszing ",
6 | "license": "MIT",
7 | "type": "module",
8 | "main": "./dist/rehype-mdx-code-props.js",
9 | "exports": "./dist/rehype-mdx-code-props.js",
10 | "repository": "remcohaszing/rehype-mdx-code-props",
11 | "bugs": "https://github.com/remcohaszing/rehype-mdx-code-props/issues",
12 | "homepage": "https://github.com/remcohaszing/rehype-mdx-code-props#readme",
13 | "funding": "https://github.com/sponsors/remcohaszing",
14 | "keywords": [
15 | "hast",
16 | "html",
17 | "markdown",
18 | "mdx",
19 | "rehype",
20 | "rehype-plugin",
21 | "unified"
22 | ],
23 | "files": [
24 | "dist",
25 | "src",
26 | "!test*"
27 | ],
28 | "scripts": {
29 | "prepack": "tsc --build",
30 | "pretest": "tsc --build",
31 | "test": "c8 node --enable-source-maps dist/test.js"
32 | },
33 | "dependencies": {
34 | "@types/hast": "^3.0.0",
35 | "hast-util-properties-to-mdx-jsx-attributes": "^1.0.0",
36 | "mdast-util-from-markdown": "^2.0.0",
37 | "mdast-util-mdx": "^3.0.0",
38 | "micromark-extension-mdxjs": "^3.0.0",
39 | "unified": "^11.0.0",
40 | "unist-util-visit-parents": "^6.0.0"
41 | },
42 | "devDependencies": {
43 | "@mdx-js/mdx": "^3.0.0",
44 | "@types/node": "^20.0.0",
45 | "@types/react": "^18.0.0",
46 | "c8": "^9.0.0",
47 | "eslint": "^8.0.0",
48 | "eslint-config-remcohaszing": "^10.0.0",
49 | "eslint-plugin-jsx-a11y": "^6.0.0",
50 | "eslint-plugin-react": "^7.0.0",
51 | "mdast-util-mdx-jsx": "^3.0.0",
52 | "prettier": "^3.0.0",
53 | "remark-cli": "12.0.0",
54 | "remark-preset-remcohaszing": "^3.0.0",
55 | "snapshot-fixtures": "^1.0.0",
56 | "typescript": "^5.0.0"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/rehype-mdx-code-props.ts:
--------------------------------------------------------------------------------
1 | import { type Root } from 'hast'
2 | import { propertiesToMdxJsxAttributes } from 'hast-util-properties-to-mdx-jsx-attributes'
3 | import { fromMarkdown } from 'mdast-util-from-markdown'
4 | import { mdxFromMarkdown } from 'mdast-util-mdx'
5 | import { type MdxJsxFlowElementHast } from 'mdast-util-mdx-jsx'
6 | import { mdxjs } from 'micromark-extension-mdxjs'
7 | import { type Plugin } from 'unified'
8 | import { visitParents } from 'unist-util-visit-parents'
9 |
10 | export interface RehypeMdxCodePropsOptions {
11 | /**
12 | * The casing to use for attribute names.
13 | *
14 | * This should match the `elementAttributeNameCase` value passed to MDX.
15 | *
16 | * @default 'react'
17 | * @see https://mdxjs.com/packages/mdx/#processoroptions
18 | */
19 | elementAttributeNameCase?: 'html' | 'react'
20 |
21 | /**
22 | * The tag name to add the attributes to.
23 | *
24 | * @default 'pre'
25 | */
26 | tagName?: 'code' | 'pre'
27 | }
28 |
29 | /**
30 | * An MDX rehype plugin for transforming markdown code meta into JSX props.
31 | */
32 | const rehypeMdxCodeProps: Plugin<[RehypeMdxCodePropsOptions?], Root> = ({
33 | elementAttributeNameCase = 'react',
34 | tagName = 'pre'
35 | } = {}) => {
36 | if (tagName !== 'code' && tagName !== 'pre') {
37 | throw new Error(`Expected tagName to be 'code' or 'pre', got: ${tagName}`)
38 | }
39 |
40 | return (ast) => {
41 | visitParents(ast, 'element', (node, ancestors) => {
42 | if (node.tagName !== 'code') {
43 | return
44 | }
45 |
46 | const meta = node.data?.meta
47 | if (typeof meta !== 'string') {
48 | return
49 | }
50 |
51 | if (!meta) {
52 | return
53 | }
54 |
55 | let child = node
56 | let parent = ancestors.at(-1)!
57 |
58 | if (tagName === 'pre') {
59 | if (parent.type !== 'element') {
60 | return
61 | }
62 |
63 | if (parent.tagName !== 'pre') {
64 | return
65 | }
66 |
67 | if (parent.children.length !== 1) {
68 | return
69 | }
70 |
71 | child = parent
72 | parent = ancestors.at(-2)!
73 | }
74 |
75 | const replacement = fromMarkdown(`<${child.tagName} ${meta} />`, {
76 | extensions: [mdxjs()],
77 | mdastExtensions: [mdxFromMarkdown()]
78 | }).children[0] as MdxJsxFlowElementHast
79 | replacement.children = child.children
80 | replacement.data = child.data
81 | replacement.position = child.position
82 | replacement.attributes.unshift(
83 | ...propertiesToMdxJsxAttributes(child.properties, { elementAttributeNameCase })
84 | )
85 |
86 | parent.children[parent.children.indexOf(child)] = replacement
87 | })
88 | }
89 | }
90 |
91 | export default rehypeMdxCodeProps
92 |
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | import assert from 'node:assert/strict'
2 | import { test } from 'node:test'
3 |
4 | import { compile } from '@mdx-js/mdx'
5 | import { type Root } from 'hast'
6 | import rehypeMdxCodeProps from 'rehype-mdx-code-props'
7 | import { testFixturesDirectory } from 'snapshot-fixtures'
8 | import { visitParents } from 'unist-util-visit-parents'
9 |
10 | testFixturesDirectory({
11 | directory: new URL('../fixtures', import.meta.url),
12 | prettier: true,
13 | tests: {
14 | 'expected-code.jsx'(file) {
15 | return compile(file, {
16 | jsx: true,
17 | rehypePlugins: [[rehypeMdxCodeProps, { tagName: 'code' }]]
18 | })
19 | },
20 |
21 | 'expected-pre.jsx'(file) {
22 | return compile(file, {
23 | jsx: true,
24 | rehypePlugins: [rehypeMdxCodeProps]
25 | })
26 | }
27 | }
28 | })
29 |
30 | test('invalid tagName', () => {
31 | assert.throws(
32 | () => compile('', { rehypePlugins: [[rehypeMdxCodeProps, { tagName: 'invalid' }]] }),
33 | new Error("Expected tagName to be 'code' or 'pre', got: invalid")
34 | )
35 | })
36 |
37 | test('meta empty string', async () => {
38 | const { value } = await compile('', {
39 | jsx: true,
40 | rehypePlugins: [
41 | () => (ast) => {
42 | ast.children.push({
43 | type: 'element',
44 | tagName: 'code',
45 | data: { meta: '' },
46 | children: []
47 | })
48 | },
49 | [rehypeMdxCodeProps, { tagName: 'code' }]
50 | ]
51 | })
52 |
53 | assert.equal(
54 | value,
55 | '/*@jsxRuntime automatic*/\n' +
56 | '/*@jsxImportSource react*/\n' +
57 | 'function _createMdxContent(props) {\n' +
58 | ' const _components = {\n' +
59 | ' code: "code",\n' +
60 | ' ...props.components\n' +
61 | ' };\n' +
62 | ' return <_components.code />;\n' +
63 | '}\n' +
64 | 'export default function MDXContent(props = {}) {\n' +
65 | ' const {wrapper: MDXLayout} = props.components || ({});\n' +
66 | ' return MDXLayout ? <_createMdxContent {...props} /> : _createMdxContent(props);\n' +
67 | '}\n'
68 | )
69 | })
70 |
71 | test('code without parent', async () => {
72 | const { value } = await compile('', {
73 | jsx: true,
74 | rehypePlugins: [
75 | () => (ast) => {
76 | ast.children.push({
77 | type: 'element',
78 | tagName: 'code',
79 | data: { meta: 'meta' },
80 | children: []
81 | })
82 | },
83 | rehypeMdxCodeProps
84 | ]
85 | })
86 |
87 | assert.equal(
88 | value,
89 | '/*@jsxRuntime automatic*/\n' +
90 | '/*@jsxImportSource react*/\n' +
91 | 'function _createMdxContent(props) {\n' +
92 | ' const _components = {\n' +
93 | ' code: "code",\n' +
94 | ' ...props.components\n' +
95 | ' };\n' +
96 | ' return <_components.code />;\n' +
97 | '}\n' +
98 | 'export default function MDXContent(props = {}) {\n' +
99 | ' const {wrapper: MDXLayout} = props.components || ({});\n' +
100 | ' return MDXLayout ? <_createMdxContent {...props} /> : _createMdxContent(props);\n' +
101 | '}\n'
102 | )
103 | })
104 |
105 | test('code with non-pre parent', async () => {
106 | const { value } = await compile('', {
107 | jsx: true,
108 | rehypePlugins: [
109 | () => (ast: Root) => {
110 | ast.children.push({
111 | type: 'element',
112 | tagName: 'div',
113 | properties: {},
114 | children: [
115 | {
116 | type: 'element',
117 | tagName: 'code',
118 | properties: {},
119 | data: { meta: 'meta' },
120 | children: []
121 | }
122 | ]
123 | })
124 | },
125 | rehypeMdxCodeProps
126 | ]
127 | })
128 |
129 | assert.equal(
130 | value,
131 | '/*@jsxRuntime automatic*/\n' +
132 | '/*@jsxImportSource react*/\n' +
133 | 'function _createMdxContent(props) {\n' +
134 | ' const _components = {\n' +
135 | ' code: "code",\n' +
136 | ' div: "div",\n' +
137 | ' ...props.components\n' +
138 | ' };\n' +
139 | ' return <_components.div><_components.code />;\n' +
140 | '}\n' +
141 | 'export default function MDXContent(props = {}) {\n' +
142 | ' const {wrapper: MDXLayout} = props.components || ({});\n' +
143 | ' return MDXLayout ? <_createMdxContent {...props} /> : _createMdxContent(props);\n' +
144 | '}\n'
145 | )
146 | })
147 |
148 | test('code with pre parent and siblings', async () => {
149 | const { value } = await compile('', {
150 | jsx: true,
151 | rehypePlugins: [
152 | () => (ast: Root) => {
153 | ast.children.push({
154 | type: 'element',
155 | tagName: 'pre',
156 | properties: {},
157 | children: [
158 | {
159 | type: 'element',
160 | tagName: 'code',
161 | properties: {},
162 | data: { meta: 'meta' },
163 | children: []
164 | },
165 | {
166 | type: 'element',
167 | tagName: 'code',
168 | properties: {},
169 | children: []
170 | }
171 | ]
172 | })
173 | },
174 | rehypeMdxCodeProps
175 | ]
176 | })
177 |
178 | assert.equal(
179 | value,
180 | '/*@jsxRuntime automatic*/\n' +
181 | '/*@jsxImportSource react*/\n' +
182 | 'function _createMdxContent(props) {\n' +
183 | ' const _components = {\n' +
184 | ' code: "code",\n' +
185 | ' pre: "pre",\n' +
186 | ' ...props.components\n' +
187 | ' };\n' +
188 | ' return <_components.pre><_components.code /><_components.code />;\n' +
189 | '}\n' +
190 | 'export default function MDXContent(props = {}) {\n' +
191 | ' const {wrapper: MDXLayout} = props.components || ({});\n' +
192 | ' return MDXLayout ? <_createMdxContent {...props} /> : _createMdxContent(props);\n' +
193 | '}\n'
194 | )
195 | })
196 |
197 | test('boolean properties true', async () => {
198 | const { value } = await compile('```js prop={prop}\n```\n', {
199 | jsx: true,
200 | rehypePlugins: [
201 | () => (ast: Root) => {
202 | visitParents(ast, { type: 'element', tagName: 'pre' }, (element) => {
203 | element.properties.hidden = true
204 | })
205 | },
206 | rehypeMdxCodeProps
207 | ]
208 | })
209 |
210 | assert.equal(
211 | value,
212 | '/*@jsxRuntime automatic*/\n' +
213 | '/*@jsxImportSource react*/\n' +
214 | 'function _createMdxContent(props) {\n' +
215 | ' const _components = {\n' +
216 | ' code: "code",\n' +
217 | ' pre: "pre",\n' +
218 | ' ...props.components\n' +
219 | ' };\n' +
220 | ' return <_components.pre hidden prop={prop}><_components.code className="language-js" />;\n' +
221 | '}\n' +
222 | 'export default function MDXContent(props = {}) {\n' +
223 | ' const {wrapper: MDXLayout} = props.components || ({});\n' +
224 | ' return MDXLayout ? <_createMdxContent {...props} /> : _createMdxContent(props);\n' +
225 | '}\n'
226 | )
227 | })
228 |
229 | test('boolean properties false', async () => {
230 | const { value } = await compile('```js prop={prop}\n```\n', {
231 | jsx: true,
232 | rehypePlugins: [
233 | () => (ast: Root) => {
234 | visitParents(ast, { type: 'element', tagName: 'pre' }, (element) => {
235 | element.properties.hidden = false
236 | })
237 | },
238 | rehypeMdxCodeProps
239 | ]
240 | })
241 |
242 | assert.equal(
243 | value,
244 | '/*@jsxRuntime automatic*/\n' +
245 | '/*@jsxImportSource react*/\n' +
246 | 'function _createMdxContent(props) {\n' +
247 | ' const _components = {\n' +
248 | ' code: "code",\n' +
249 | ' pre: "pre",\n' +
250 | ' ...props.components\n' +
251 | ' };\n' +
252 | ' return <_components.pre prop={prop}><_components.code className="language-js" />;\n' +
253 | '}\n' +
254 | 'export default function MDXContent(props = {}) {\n' +
255 | ' const {wrapper: MDXLayout} = props.components || ({});\n' +
256 | ' return MDXLayout ? <_createMdxContent {...props} /> : _createMdxContent(props);\n' +
257 | '}\n'
258 | )
259 | })
260 |
261 | test('boolean properties empty', async () => {
262 | const { value } = await compile('```js prop={prop}\n```\n', {
263 | jsx: true,
264 | rehypePlugins: [
265 | () => (ast: Root) => {
266 | visitParents(ast, { type: 'element', tagName: 'pre' }, (element) => {
267 | element.properties.hidden = ''
268 | })
269 | },
270 | rehypeMdxCodeProps
271 | ]
272 | })
273 |
274 | assert.equal(
275 | value,
276 | '/*@jsxRuntime automatic*/\n' +
277 | '/*@jsxImportSource react*/\n' +
278 | 'function _createMdxContent(props) {\n' +
279 | ' const _components = {\n' +
280 | ' code: "code",\n' +
281 | ' pre: "pre",\n' +
282 | ' ...props.components\n' +
283 | ' };\n' +
284 | ' return <_components.pre prop={prop}><_components.code className="language-js" />;\n' +
285 | '}\n' +
286 | 'export default function MDXContent(props = {}) {\n' +
287 | ' const {wrapper: MDXLayout} = props.components || ({});\n' +
288 | ' return MDXLayout ? <_createMdxContent {...props} /> : _createMdxContent(props);\n' +
289 | '}\n'
290 | )
291 | })
292 |
293 | test('numeric properties', async () => {
294 | const { value } = await compile('```js prop={prop}\n```\n', {
295 | jsx: true,
296 | rehypePlugins: [
297 | () => (ast: Root) => {
298 | visitParents(ast, { type: 'element', tagName: 'pre' }, (element) => {
299 | element.properties.height = 42
300 | })
301 | },
302 | rehypeMdxCodeProps
303 | ]
304 | })
305 |
306 | assert.equal(
307 | value,
308 | '/*@jsxRuntime automatic*/\n' +
309 | '/*@jsxImportSource react*/\n' +
310 | 'function _createMdxContent(props) {\n' +
311 | ' const _components = {\n' +
312 | ' code: "code",\n' +
313 | ' pre: "pre",\n' +
314 | ' ...props.components\n' +
315 | ' };\n' +
316 | ' return <_components.pre height="42" prop={prop}><_components.code className="language-js" />;\n' +
317 | '}\n' +
318 | 'export default function MDXContent(props = {}) {\n' +
319 | ' const {wrapper: MDXLayout} = props.components || ({});\n' +
320 | ' return MDXLayout ? <_createMdxContent {...props} /> : _createMdxContent(props);\n' +
321 | '}\n'
322 | )
323 | })
324 |
325 | test('style property', async () => {
326 | const { value } = await compile('```js prop={prop}\n```\n', {
327 | jsx: true,
328 | rehypePlugins: [
329 | () => (ast: Root) => {
330 | visitParents(ast, { type: 'element', tagName: 'pre' }, (element) => {
331 | element.properties.style = 'background-color:tomato;'
332 | })
333 | },
334 | rehypeMdxCodeProps
335 | ]
336 | })
337 |
338 | assert.equal(
339 | value,
340 | '/*@jsxRuntime automatic*/\n' +
341 | '/*@jsxImportSource react*/\n' +
342 | 'function _createMdxContent(props) {\n' +
343 | ' const _components = {\n' +
344 | ' code: "code",\n' +
345 | ' pre: "pre",\n' +
346 | ' ...props.components\n' +
347 | ' };\n' +
348 | ' return <_components.pre style={{\n' +
349 | ' "backgroundColor": "tomato"\n' +
350 | ' }} prop={prop}><_components.code className="language-js" />;\n' +
351 | '}\n' +
352 | 'export default function MDXContent(props = {}) {\n' +
353 | ' const {wrapper: MDXLayout} = props.components || ({});\n' +
354 | ' return MDXLayout ? <_createMdxContent {...props} /> : _createMdxContent(props);\n' +
355 | '}\n'
356 | )
357 | })
358 |
359 | test('comma separated properties', async () => {
360 | const { value } = await compile('```js prop={prop}\n```\n', {
361 | jsx: true,
362 | rehypePlugins: [
363 | () => (ast: Root) => {
364 | visitParents(ast, { type: 'element', tagName: 'pre' }, (element) => {
365 | element.properties.accept = ['a', 'b']
366 | })
367 | },
368 | rehypeMdxCodeProps
369 | ]
370 | })
371 |
372 | assert.equal(
373 | value,
374 | '/*@jsxRuntime automatic*/\n' +
375 | '/*@jsxImportSource react*/\n' +
376 | 'function _createMdxContent(props) {\n' +
377 | ' const _components = {\n' +
378 | ' code: "code",\n' +
379 | ' pre: "pre",\n' +
380 | ' ...props.components\n' +
381 | ' };\n' +
382 | ' return <_components.pre accept="a, b" prop={prop}><_components.code className="language-js" />;\n' +
383 | '}\n' +
384 | 'export default function MDXContent(props = {}) {\n' +
385 | ' const {wrapper: MDXLayout} = props.components || ({});\n' +
386 | ' return MDXLayout ? <_createMdxContent {...props} /> : _createMdxContent(props);\n' +
387 | '}\n'
388 | )
389 | })
390 |
391 | test('elementAttributeNameCase react', async () => {
392 | const { value } = await compile('```js prop={prop}\n```\n', {
393 | jsx: true,
394 | rehypePlugins: [
395 | () => (ast: Root) => {
396 | visitParents(ast, { type: 'element', tagName: 'pre' }, (element) => {
397 | element.properties.itemId = 'a'
398 | })
399 | },
400 | rehypeMdxCodeProps
401 | ]
402 | })
403 |
404 | assert.equal(
405 | value,
406 | '/*@jsxRuntime automatic*/\n' +
407 | '/*@jsxImportSource react*/\n' +
408 | 'function _createMdxContent(props) {\n' +
409 | ' const _components = {\n' +
410 | ' code: "code",\n' +
411 | ' pre: "pre",\n' +
412 | ' ...props.components\n' +
413 | ' };\n' +
414 | ' return <_components.pre itemID="a" prop={prop}><_components.code className="language-js" />;\n' +
415 | '}\n' +
416 | 'export default function MDXContent(props = {}) {\n' +
417 | ' const {wrapper: MDXLayout} = props.components || ({});\n' +
418 | ' return MDXLayout ? <_createMdxContent {...props} /> : _createMdxContent(props);\n' +
419 | '}\n'
420 | )
421 | })
422 |
423 | test('elementAttributeNameCase html', async () => {
424 | const { value } = await compile('```js prop={prop}\n```\n', {
425 | jsx: true,
426 | rehypePlugins: [
427 | () => (ast: Root) => {
428 | visitParents(ast, { type: 'element', tagName: 'pre' }, (element) => {
429 | element.properties.itemId = 'a'
430 | })
431 | },
432 | [rehypeMdxCodeProps, { elementAttributeNameCase: 'html' }]
433 | ]
434 | })
435 |
436 | assert.equal(
437 | value,
438 | '/*@jsxRuntime automatic*/\n' +
439 | '/*@jsxImportSource react*/\n' +
440 | 'function _createMdxContent(props) {\n' +
441 | ' const _components = {\n' +
442 | ' code: "code",\n' +
443 | ' pre: "pre",\n' +
444 | ' ...props.components\n' +
445 | ' };\n' +
446 | ' return <_components.pre itemId="a" prop={prop}><_components.code className="language-js" />;\n' +
447 | '}\n' +
448 | 'export default function MDXContent(props = {}) {\n' +
449 | ' const {wrapper: MDXLayout} = props.components || ({});\n' +
450 | ' return MDXLayout ? <_createMdxContent {...props} /> : _createMdxContent(props);\n' +
451 | '}\n'
452 | )
453 | })
454 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "declarationMap": true,
5 | "module": "node16",
6 | "outDir": "dist",
7 | "rootDir": "src",
8 | "sourceMap": true,
9 | "strict": true,
10 | "stripInternal": true,
11 | "target": "es2018"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------