├── .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 | [![github actions](https://github.com/remcohaszing/rehype-mdx-code-props/actions/workflows/ci.yaml/badge.svg)](https://github.com/remcohaszing/rehype-mdx-code-props/actions/workflows/ci.yaml) 4 | [![codecov](https://codecov.io/gh/remcohaszing/rehype-mdx-code-props/branch/main/graph/badge.svg)](https://codecov.io/gh/remcohaszing/rehype-mdx-code-props) 5 | [![npm version](https://img.shields.io/npm/v/rehype-mdx-code-props)](https://www.npmjs.com/package/rehype-mdx-code-props) 6 | [![npm downloads](https://img.shields.io/npm/dm/rehype-mdx-code-props)](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 | 


--------------------------------------------------------------------------------