├── .c8rc.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.yaml ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .prettierrc.yaml ├── .remarkignore ├── .remarkrc.yaml ├── LICENSE.md ├── README.md ├── fixtures ├── alt │ ├── expected.jsx │ └── input.md ├── custom-attributes │ ├── expected.jsx │ ├── input.md │ └── options.json ├── duplicate │ ├── expected.jsx │ └── input.md ├── external-http │ ├── expected.jsx │ └── input.md ├── external-https │ ├── expected.jsx │ └── input.md ├── external-without-protocol │ ├── expected.jsx │ └── input.md ├── hash-preserve-both │ ├── expected.jsx │ ├── input.md │ └── options.json ├── hash-preserve-import │ ├── expected.jsx │ └── input.md ├── hash-preserve-jsx │ ├── expected.jsx │ ├── input.md │ └── options.json ├── hash-preserve-none │ ├── expected.jsx │ ├── input.md │ └── options.json ├── html │ ├── expected.jsx │ └── input.md ├── inline │ ├── expected.jsx │ └── input.md ├── link │ ├── expected.jsx │ └── input.md ├── multiple │ ├── expected.jsx │ └── input.md ├── node-modules │ ├── expected.jsx │ ├── input.md │ └── options.json ├── query-preserve-both │ ├── expected.jsx │ ├── input.md │ └── options.json ├── query-preserve-import │ ├── expected.jsx │ └── input.md ├── query-preserve-jsx │ ├── expected.jsx │ ├── input.md │ └── options.json ├── query-preserve-none │ ├── expected.jsx │ ├── input.md │ └── options.json ├── relative-grandparent │ ├── expected.jsx │ └── input.md ├── relative-parent │ ├── expected.jsx │ └── input.md ├── relative-sibling │ ├── expected.jsx │ └── input.md ├── relative-simple │ ├── expected.jsx │ └── input.md ├── root │ ├── expected.jsx │ └── input.md ├── srcset │ ├── expected.jsx │ └── input.md └── title │ ├── expected.jsx │ └── input.md ├── package-lock.json ├── package.json ├── src ├── rehype-mdx-import-media.test.ts └── rehype-mdx-import-media.ts └── tsconfig.json /.c8rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "100": true, 3 | "reporter": ["html", "lcov", "text"] 4 | } 5 | -------------------------------------------------------------------------------- /.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 | import/no-extraneous-dependencies: off 7 | overrides: 8 | - files: ['*.md/*'] 9 | rules: 10 | capitalized-comments: off 11 | -------------------------------------------------------------------------------- /.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 | test: 35 | runs-on: ubuntu-latest 36 | strategy: 37 | matrix: 38 | node-version: 39 | - 18 40 | - 20 41 | - 22 42 | steps: 43 | - uses: actions/checkout@v4 44 | - uses: actions/setup-node@v4 45 | with: 46 | node-version: ${{ matrix.node-version }} 47 | - run: npm ci 48 | - run: npm test 49 | - uses: codecov/codecov-action@v4 50 | if: ${{ matrix.node-version == 20 }} 51 | 52 | prettier: 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: actions/checkout@v4 56 | - uses: actions/setup-node@v4 57 | with: 58 | node-version: 20 59 | - run: npm ci 60 | - run: npx prettier --check . 61 | 62 | release: 63 | runs-on: ubuntu-latest 64 | needs: 65 | - eslint 66 | - test 67 | - pack 68 | - prettier 69 | if: startsWith(github.ref, 'refs/tags/') 70 | permissions: 71 | id-token: write 72 | steps: 73 | - uses: actions/setup-node@v4 74 | with: 75 | node-version: 20 76 | registry-url: https://registry.npmjs.org 77 | - uses: actions/download-artifact@v4 78 | with: { name: package } 79 | - run: npm publish *.tgz --provenance 80 | env: 81 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | dist/ 3 | node_modules/ 4 | *.tsbuildinfo 5 | *.log 6 | *.tgz 7 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | proseWrap: always 2 | semi: false 3 | singleQuote: true 4 | trailingComma: none 5 | -------------------------------------------------------------------------------- /.remarkignore: -------------------------------------------------------------------------------- 1 | fixtures/ 2 | -------------------------------------------------------------------------------- /.remarkrc.yaml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - remark-preset-remcohaszing 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright © 2024 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-import-media 2 | 3 | [![github actions](https://github.com/remcohaszing/rehype-mdx-import-media/actions/workflows/ci.yaml/badge.svg)](https://github.com/remcohaszing/rehype-mdx-import-media/actions/workflows/ci.yaml) 4 | [![codecov](https://codecov.io/gh/remcohaszing/rehype-mdx-import-media/branch/main/graph/badge.svg)](https://codecov.io/gh/remcohaszing/rehype-mdx-import-media) 5 | [![npm version](https://img.shields.io/npm/v/rehype-mdx-import-media)](https://www.npmjs.com/package/rehype-mdx-import-media) 6 | [![npm downloads](https://img.shields.io/npm/dm/rehype-mdx-import-media)](https://www.npmjs.com/package/rehype-mdx-import-media) 7 | 8 | An [MDX](https://mdxjs.com) [rehype](https://github.com/rehypejs/rehype) plugin for turning media 9 | paths into imports. 10 | 11 | ## Table of Contents 12 | 13 | - [Installation](#installation) 14 | - [When should I use this?](#when-should-i-use-this) 15 | - [Usage](#usage) 16 | - [Examples](#examples) 17 | - [Script](#script) 18 | - [Next.js](#nextjs) 19 | - [API](#api) 20 | - [Options](#options) 21 | - [Compatibility](#compatibility) 22 | - [License](#license) 23 | 24 | ## Installation 25 | 26 | ```sh 27 | npm install rehype-mdx-import-media 28 | ``` 29 | 30 | ## When should I use this? 31 | 32 | You may want to author images in MDX using the markdown format, like so: 33 | 34 | ```markdown 35 | ![alt](./image.png 'title') 36 | ``` 37 | 38 | You may use MDX with a bundler such as [webpack](https://webpack.js.org) or 39 | [Vite](http://vitejs.dev). By default bundlers don’t understand how to resolve those images. They 40 | only understand how to resolve imports. This plugin solves that problem. 41 | 42 | Also you may use MDX to load markdown files. If you reference other media in those markdown files 43 | using HTML tags, that media can be resolved by this plugin too. 44 | 45 | ## Usage 46 | 47 | This plugin takes HTML elements that refer to media content, and turns them into MDX expressions 48 | that use imports. This allows bundlers to resolve media you referenced from your code. Note that JSX 49 | elements are **not** HTML elements, so they are not processed. HTML elements can come from: 50 | 51 | - Markdown syntax in MDX files, such as images. 52 | - HTML in files parsed using the `md` [format](https://mdxjs.com/packages/mdx/#processoroptions) 53 | when using [`rehype-raw`](https://github.com/rehypejs/rehype-raw) 54 | - Custom remark / rehype plugins. 55 | 56 | If this plugin finds an attribute to process, it transforms the 57 | [hast](https://github.com/syntax-tree/hast) [`element`](https://github.com/syntax-tree/hast#element) 58 | nodes into an 59 | [`mdxJsxTextElement`](https://github.com/syntax-tree/mdast-util-mdx-jsx#mdxjsxtextelementhast) node. 60 | This may prevent other rehype plugins from further processing. To avoid this, put 61 | `rehype-mdx-import-media` after any other rehype plugins 62 | 63 | ## Examples 64 | 65 | ### Script 66 | 67 | Let’s say we have a file named `example.mdx` with the following contents: 68 | 69 | ```mdx 70 | ![](./image.png) 71 | ``` 72 | 73 | The following script: 74 | 75 | ```js 76 | import { compile } from '@mdx-js/mdx' 77 | import rehypeMdxImportMedia from 'rehype-mdx-import-media' 78 | import { read } from 'to-vfile' 79 | 80 | const { value } = await compile(await read('example.mdx'), { 81 | jsx: true, 82 | rehypePlugins: [rehypeMdxImportMedia] 83 | }) 84 | console.log(value) 85 | ``` 86 | 87 | Roughly yields: 88 | 89 | ```jsx 90 | import _rehypeMdxImportMedia0 from './image.png' 91 | 92 | export default function MDXContent() { 93 | return ( 94 |

95 | 96 |

97 | ) 98 | } 99 | ``` 100 | 101 | ### Next.js 102 | 103 | If you use this with Next.js, you must combine it with 104 | [`next/image`](https://nextjs.org/docs/pages/api-reference/components/image). 105 | 106 | ```ts 107 | // next.config.ts 108 | import createMDX from '@next/mdx' 109 | 110 | const withMDX = createMDX({ 111 | options: { 112 | rehypePlugins: [['rehype-mdx-import-media']] 113 | } 114 | }) 115 | 116 | export default withMDX() 117 | ``` 118 | 119 | ```ts 120 | // mdx-components.ts 121 | import Image from 'next/image' 122 | 123 | const components = { 124 | img: Image 125 | } 126 | 127 | declare global { 128 | type MDXProvidedComponents = typeof components 129 | } 130 | 131 | export function useMDXComponents(): MDXProvidedComponents { 132 | return components 133 | } 134 | ``` 135 | 136 | ## API 137 | 138 | The default export is a [rehype](https://github.com/rehypejs/rehype) plugin. 139 | 140 | ### Options 141 | 142 | - `attributes` (`object`): HTML element attributes that should be processed. The key is the HTML 143 | element tag name. The value is a list of attribute names to process. The default attributes are: 144 | - [`audio[src]`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio#src) 145 | - [`embed[src]`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/embed#src) 146 | - [`img[src]`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#src) 147 | - [`img[srcset]`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#srcset) 148 | - [`object[data]`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object#data) 149 | - [`source[src]`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/source#src) 150 | - [`source[srcset]`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/source#srcset) 151 | - [`track[src]`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track#src) 152 | - [`video[poster]`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#poster) 153 | - [`video[src]`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#src) 154 | - `elementAttributeNameCase` (`'html' | 'react'`): The casing to use for attribute names. This 155 | should match the elementAttributeNameCase value passed to MDX. (Default: `'react'`) 156 | - `preserveHash` (`'both' | 'import' | 'jsx' | 'none'`): Where to keep URL hash. (Default: 157 | `'import'`) 158 | - `both`: Keep the URL hash on both the import source and the JSX prop. 159 | - `import`: Only keep the URL hash on the import source. 160 | - `jsx`: Only keep the URL hash on the JSX prop. 161 | - `none`: Remove the URL hash. 162 | - `preserveQuery` (`'both' | 'import' | 'jsx' | 'none'`): Where to keep query parameters. (Default: 163 | `'import'`) 164 | - `both`: Keep the query parameters on both the import source and the JSX prop. 165 | - `import`: Only keep the query parameters on the import source. 166 | - `jsx`: Only keep the query parameters on the JSX prop. 167 | - `none`: Remove the query parameters. 168 | - `resolve` (`boolean`): By default imports are resolved relative to the markdown file. This matches 169 | behaviour of places that render the markdown, such as GitHub. If this is set to false, this 170 | behaviour is removed and URLs are no longer processed. This allows to import images from 171 | `node_modules`. If this is disabled, local images can still be imported by prepending the path 172 | with `./.`. (Default: `true`). 173 | 174 | ## Compatibility 175 | 176 | This project is compatible with MDX 3 and Node.js 18 or greater. 177 | 178 | ## License 179 | 180 | [MIT](LICENSE.md) © [Remco Haszing](https://github.com/remcohaszing) 181 | -------------------------------------------------------------------------------- /fixtures/alt/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from './image.png' 4 | function _createMdxContent(props) { 5 | const _components = { 6 | img: 'img', 7 | p: 'p', 8 | ...props.components 9 | } 10 | return ( 11 | <_components.p> 12 | <_components.img src={_rehypeMdxImportMedia0} alt="Alt text" /> 13 | 14 | ) 15 | } 16 | export default function MDXContent(props = {}) { 17 | const { wrapper: MDXLayout } = props.components || {} 18 | return MDXLayout ? ( 19 | 20 | <_createMdxContent {...props} /> 21 | 22 | ) : ( 23 | _createMdxContent(props) 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /fixtures/alt/input.md: -------------------------------------------------------------------------------- 1 | ![Alt text](image.png) 2 | -------------------------------------------------------------------------------- /fixtures/custom-attributes/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from './script.js' 4 | function _createMdxContent(props) { 5 | const _components = { 6 | img: 'img', 7 | p: 'p', 8 | script: 'script', 9 | ...props.components 10 | } 11 | return ( 12 | <> 13 | <_components.p> 14 | <_components.img src="./hello.png" alt="" /> 15 | 16 | {'\n'} 17 | <_components.script src={_rehypeMdxImportMedia0} /> 18 | 19 | ) 20 | } 21 | export default function MDXContent(props = {}) { 22 | const { wrapper: MDXLayout } = props.components || {} 23 | return MDXLayout ? ( 24 | 25 | <_createMdxContent {...props} /> 26 | 27 | ) : ( 28 | _createMdxContent(props) 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /fixtures/custom-attributes/input.md: -------------------------------------------------------------------------------- 1 | ![](./hello.png) 2 | 3 | 4 | -------------------------------------------------------------------------------- /fixtures/custom-attributes/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": { 3 | "script": "src" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /fixtures/duplicate/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from './image.png' 4 | import _rehypeMdxImportMedia1 from './image.jpg' 5 | function _createMdxContent(props) { 6 | const _components = { 7 | img: 'img', 8 | p: 'p', 9 | ...props.components 10 | } 11 | return ( 12 | <> 13 | <_components.p> 14 | <_components.img src={_rehypeMdxImportMedia0} alt="" /> 15 | 16 | {'\n'} 17 | <_components.p> 18 | <_components.img src={_rehypeMdxImportMedia0} alt="" /> 19 | 20 | {'\n'} 21 | <_components.p> 22 | <_components.img src={_rehypeMdxImportMedia1} alt="" /> 23 | 24 | {'\n'} 25 | <_components.p> 26 | <_components.img src={_rehypeMdxImportMedia1} alt="" /> 27 | 28 | 29 | ) 30 | } 31 | export default function MDXContent(props = {}) { 32 | const { wrapper: MDXLayout } = props.components || {} 33 | return MDXLayout ? ( 34 | 35 | <_createMdxContent {...props} /> 36 | 37 | ) : ( 38 | _createMdxContent(props) 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /fixtures/duplicate/input.md: -------------------------------------------------------------------------------- 1 | ![](image.png) 2 | 3 | ![](image.png) 4 | 5 | ![](image.jpg) 6 | 7 | ![](image.jpg) 8 | -------------------------------------------------------------------------------- /fixtures/external-http/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | function _createMdxContent(props) { 4 | const _components = { 5 | img: 'img', 6 | p: 'p', 7 | ...props.components 8 | } 9 | return ( 10 | <_components.p> 11 | <_components.img src="http://mdx-logo.now.sh" alt="" /> 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/external-http/input.md: -------------------------------------------------------------------------------- 1 | ![](http://mdx-logo.now.sh) 2 | -------------------------------------------------------------------------------- /fixtures/external-https/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | function _createMdxContent(props) { 4 | const _components = { 5 | img: 'img', 6 | p: 'p', 7 | ...props.components 8 | } 9 | return ( 10 | <_components.p> 11 | <_components.img src="https://mdx-logo.now.sh" alt="" /> 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/external-https/input.md: -------------------------------------------------------------------------------- 1 | ![](https://mdx-logo.now.sh) 2 | -------------------------------------------------------------------------------- /fixtures/external-without-protocol/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | function _createMdxContent(props) { 4 | const _components = { 5 | img: 'img', 6 | p: 'p', 7 | ...props.components 8 | } 9 | return ( 10 | <_components.p> 11 | <_components.img src="//mdx-logo.now.sh" alt="" /> 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/external-without-protocol/input.md: -------------------------------------------------------------------------------- 1 | ![](//mdx-logo.now.sh) 2 | -------------------------------------------------------------------------------- /fixtures/hash-preserve-both/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from './image.png#some-hash' 4 | import _rehypeMdxImportMedia1 from './image.png#some-hash2' 5 | function _createMdxContent(props) { 6 | const _components = { 7 | img: 'img', 8 | p: 'p', 9 | ...props.components 10 | } 11 | return ( 12 | <> 13 | <_components.p> 14 | <_components.img src={`${_rehypeMdxImportMedia0}#some-hash`} alt="" /> 15 | 16 | {'\n'} 17 | <_components.img 18 | srcSet={`${_rehypeMdxImportMedia0}#some-hash 2x,${_rehypeMdxImportMedia1}#some-hash2 4x`} 19 | /> 20 | 21 | ) 22 | } 23 | export default function MDXContent(props = {}) { 24 | const { wrapper: MDXLayout } = props.components || {} 25 | return MDXLayout ? ( 26 | 27 | <_createMdxContent {...props} /> 28 | 29 | ) : ( 30 | _createMdxContent(props) 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /fixtures/hash-preserve-both/input.md: -------------------------------------------------------------------------------- 1 | ![](image.png#some-hash) 2 | 3 | 4 | -------------------------------------------------------------------------------- /fixtures/hash-preserve-both/options.json: -------------------------------------------------------------------------------- 1 | { "preserveHash": "both" } 2 | -------------------------------------------------------------------------------- /fixtures/hash-preserve-import/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from './image.png#some-hash' 4 | import _rehypeMdxImportMedia1 from './image.png#some-hash2' 5 | function _createMdxContent(props) { 6 | const _components = { 7 | img: 'img', 8 | p: 'p', 9 | ...props.components 10 | } 11 | return ( 12 | <> 13 | <_components.p> 14 | <_components.img src={_rehypeMdxImportMedia0} alt="" /> 15 | 16 | {'\n'} 17 | <_components.img srcSet={`${_rehypeMdxImportMedia0} 2x,${_rehypeMdxImportMedia1} 4x`} /> 18 | 19 | ) 20 | } 21 | export default function MDXContent(props = {}) { 22 | const { wrapper: MDXLayout } = props.components || {} 23 | return MDXLayout ? ( 24 | 25 | <_createMdxContent {...props} /> 26 | 27 | ) : ( 28 | _createMdxContent(props) 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /fixtures/hash-preserve-import/input.md: -------------------------------------------------------------------------------- 1 | ![](image.png#some-hash) 2 | 3 | 4 | -------------------------------------------------------------------------------- /fixtures/hash-preserve-jsx/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from './image.png' 4 | function _createMdxContent(props) { 5 | const _components = { 6 | img: 'img', 7 | p: 'p', 8 | ...props.components 9 | } 10 | return ( 11 | <> 12 | <_components.p> 13 | <_components.img src={`${_rehypeMdxImportMedia0}#some-hash`} alt="" /> 14 | 15 | {'\n'} 16 | <_components.img 17 | srcSet={`${_rehypeMdxImportMedia0}#some-hash 2x,${_rehypeMdxImportMedia0}#some-hash2 4x`} 18 | /> 19 | 20 | ) 21 | } 22 | export default function MDXContent(props = {}) { 23 | const { wrapper: MDXLayout } = props.components || {} 24 | return MDXLayout ? ( 25 | 26 | <_createMdxContent {...props} /> 27 | 28 | ) : ( 29 | _createMdxContent(props) 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /fixtures/hash-preserve-jsx/input.md: -------------------------------------------------------------------------------- 1 | ![](image.png#some-hash) 2 | 3 | 4 | -------------------------------------------------------------------------------- /fixtures/hash-preserve-jsx/options.json: -------------------------------------------------------------------------------- 1 | { "preserveHash": "jsx" } 2 | -------------------------------------------------------------------------------- /fixtures/hash-preserve-none/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from './image.png' 4 | function _createMdxContent(props) { 5 | const _components = { 6 | img: 'img', 7 | p: 'p', 8 | ...props.components 9 | } 10 | return ( 11 | <> 12 | <_components.p> 13 | <_components.img src={_rehypeMdxImportMedia0} alt="" /> 14 | 15 | {'\n'} 16 | <_components.img srcSet={`${_rehypeMdxImportMedia0} 2x,${_rehypeMdxImportMedia0} 4x`} /> 17 | 18 | ) 19 | } 20 | export default function MDXContent(props = {}) { 21 | const { wrapper: MDXLayout } = props.components || {} 22 | return MDXLayout ? ( 23 | 24 | <_createMdxContent {...props} /> 25 | 26 | ) : ( 27 | _createMdxContent(props) 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /fixtures/hash-preserve-none/input.md: -------------------------------------------------------------------------------- 1 | ![](image.png#some-hash) 2 | 3 | 4 | -------------------------------------------------------------------------------- /fixtures/hash-preserve-none/options.json: -------------------------------------------------------------------------------- 1 | { "preserveHash": "none" } 2 | -------------------------------------------------------------------------------- /fixtures/html/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from './image.png' 4 | import _rehypeMdxImportMedia1 from './other-image.png' 5 | function _createMdxContent(props) { 6 | const _components = { 7 | img: 'img', 8 | picture: 'picture', 9 | video: 'video', 10 | ...props.components 11 | } 12 | return ( 13 | <> 14 | <_components.img 15 | srcSet={`${_rehypeMdxImportMedia0} 2x,${_rehypeMdxImportMedia0} 640w 480h,${_rehypeMdxImportMedia1}`} 16 | /> 17 | {'\n'} 18 | <_components.picture> 19 | {'\n '} 20 | <_components.img 21 | srcSet={`${_rehypeMdxImportMedia0} 2x,${_rehypeMdxImportMedia0} 640w 480h,${_rehypeMdxImportMedia1}`} 22 | /> 23 | {'\n'} 24 | 25 | {'\n'} 26 | <_components.video>{'\n'} 27 | 28 | ) 29 | } 30 | export default function MDXContent(props = {}) { 31 | const { wrapper: MDXLayout } = props.components || {} 32 | return MDXLayout ? ( 33 | 34 | <_createMdxContent {...props} /> 35 | 36 | ) : ( 37 | _createMdxContent(props) 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /fixtures/html/input.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /fixtures/inline/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from './image.png' 4 | function _createMdxContent(props) { 5 | const _components = { 6 | img: 'img', 7 | p: 'p', 8 | ...props.components 9 | } 10 | return ( 11 | <_components.p> 12 | {'This is an inline image: '} 13 | <_components.img src={_rehypeMdxImportMedia0} alt="" /> 14 | {'. See?'} 15 | 16 | ) 17 | } 18 | export default function MDXContent(props = {}) { 19 | const { wrapper: MDXLayout } = props.components || {} 20 | return MDXLayout ? ( 21 | 22 | <_createMdxContent {...props} /> 23 | 24 | ) : ( 25 | _createMdxContent(props) 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /fixtures/inline/input.md: -------------------------------------------------------------------------------- 1 | This is an inline image: ![](image.png). See? 2 | -------------------------------------------------------------------------------- /fixtures/link/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from './image.png' 4 | function _createMdxContent(props) { 5 | const _components = { 6 | a: 'a', 7 | img: 'img', 8 | p: 'p', 9 | ...props.components 10 | } 11 | return ( 12 | <_components.p> 13 | <_components.a href="https://example.com"> 14 | <_components.img src={_rehypeMdxImportMedia0} alt="" /> 15 | 16 | 17 | ) 18 | } 19 | export default function MDXContent(props = {}) { 20 | const { wrapper: MDXLayout } = props.components || {} 21 | return MDXLayout ? ( 22 | 23 | <_createMdxContent {...props} /> 24 | 25 | ) : ( 26 | _createMdxContent(props) 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /fixtures/link/input.md: -------------------------------------------------------------------------------- 1 | [![](image.png)](https://example.com) 2 | -------------------------------------------------------------------------------- /fixtures/multiple/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from './image.gif' 4 | import _rehypeMdxImportMedia1 from './image.jpg' 5 | import _rehypeMdxImportMedia2 from './image.png' 6 | import _rehypeMdxImportMedia3 from './image.svg' 7 | function _createMdxContent(props) { 8 | const _components = { 9 | img: 'img', 10 | p: 'p', 11 | ...props.components 12 | } 13 | return ( 14 | <> 15 | <_components.p> 16 | <_components.img src={_rehypeMdxImportMedia0} alt="" /> 17 | 18 | {'\n'} 19 | <_components.p> 20 | <_components.img src={_rehypeMdxImportMedia1} alt="" /> 21 | 22 | {'\n'} 23 | <_components.p> 24 | <_components.img src={_rehypeMdxImportMedia2} alt="" /> 25 | 26 | {'\n'} 27 | <_components.p> 28 | <_components.img src={_rehypeMdxImportMedia3} alt="" /> 29 | 30 | 31 | ) 32 | } 33 | export default function MDXContent(props = {}) { 34 | const { wrapper: MDXLayout } = props.components || {} 35 | return MDXLayout ? ( 36 | 37 | <_createMdxContent {...props} /> 38 | 39 | ) : ( 40 | _createMdxContent(props) 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /fixtures/multiple/input.md: -------------------------------------------------------------------------------- 1 | ![](image.gif) 2 | 3 | ![](image.jpg) 4 | 5 | ![](image.png) 6 | 7 | ![](image.svg) 8 | -------------------------------------------------------------------------------- /fixtures/node-modules/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from '@browser-logos/chrome/chrome.png' 4 | function _createMdxContent(props) { 5 | const _components = { 6 | img: 'img', 7 | p: 'p', 8 | ...props.components 9 | } 10 | return ( 11 | <_components.p> 12 | <_components.img src={_rehypeMdxImportMedia0} alt="" /> 13 | 14 | ) 15 | } 16 | export default function MDXContent(props = {}) { 17 | const { wrapper: MDXLayout } = props.components || {} 18 | return MDXLayout ? ( 19 | 20 | <_createMdxContent {...props} /> 21 | 22 | ) : ( 23 | _createMdxContent(props) 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /fixtures/node-modules/input.md: -------------------------------------------------------------------------------- 1 | ![](@browser-logos/chrome/chrome.png) 2 | -------------------------------------------------------------------------------- /fixtures/node-modules/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "resolve": false 3 | } 4 | -------------------------------------------------------------------------------- /fixtures/query-preserve-both/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from './image.png?size=16' 4 | import _rehypeMdxImportMedia1 from './image.png?size=32' 5 | function _createMdxContent(props) { 6 | const _components = { 7 | img: 'img', 8 | p: 'p', 9 | ...props.components 10 | } 11 | return ( 12 | <> 13 | <_components.p> 14 | <_components.img src={`${_rehypeMdxImportMedia0}?size=16`} alt="" /> 15 | 16 | {'\n'} 17 | <_components.img 18 | srcSet={`${_rehypeMdxImportMedia0}?size=16 2x,${_rehypeMdxImportMedia1}?size=32 4x`} 19 | /> 20 | 21 | ) 22 | } 23 | export default function MDXContent(props = {}) { 24 | const { wrapper: MDXLayout } = props.components || {} 25 | return MDXLayout ? ( 26 | 27 | <_createMdxContent {...props} /> 28 | 29 | ) : ( 30 | _createMdxContent(props) 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /fixtures/query-preserve-both/input.md: -------------------------------------------------------------------------------- 1 | ![](image.png?size=16) 2 | 3 | 4 | -------------------------------------------------------------------------------- /fixtures/query-preserve-both/options.json: -------------------------------------------------------------------------------- 1 | { "preserveQuery": "both" } 2 | -------------------------------------------------------------------------------- /fixtures/query-preserve-import/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from './image.png?size=16' 4 | import _rehypeMdxImportMedia1 from './image.png?size=32' 5 | function _createMdxContent(props) { 6 | const _components = { 7 | img: 'img', 8 | p: 'p', 9 | ...props.components 10 | } 11 | return ( 12 | <> 13 | <_components.p> 14 | <_components.img src={_rehypeMdxImportMedia0} alt="" /> 15 | 16 | {'\n'} 17 | <_components.img srcSet={`${_rehypeMdxImportMedia0} 2x,${_rehypeMdxImportMedia1} 4x`} /> 18 | 19 | ) 20 | } 21 | export default function MDXContent(props = {}) { 22 | const { wrapper: MDXLayout } = props.components || {} 23 | return MDXLayout ? ( 24 | 25 | <_createMdxContent {...props} /> 26 | 27 | ) : ( 28 | _createMdxContent(props) 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /fixtures/query-preserve-import/input.md: -------------------------------------------------------------------------------- 1 | ![](image.png?size=16) 2 | 3 | 4 | -------------------------------------------------------------------------------- /fixtures/query-preserve-jsx/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from './image.png' 4 | function _createMdxContent(props) { 5 | const _components = { 6 | img: 'img', 7 | p: 'p', 8 | ...props.components 9 | } 10 | return ( 11 | <> 12 | <_components.p> 13 | <_components.img src={`${_rehypeMdxImportMedia0}?size=16`} alt="" /> 14 | 15 | {'\n'} 16 | <_components.img 17 | srcSet={`${_rehypeMdxImportMedia0}?size=16 2x,${_rehypeMdxImportMedia0}?size=32 4x`} 18 | /> 19 | 20 | ) 21 | } 22 | export default function MDXContent(props = {}) { 23 | const { wrapper: MDXLayout } = props.components || {} 24 | return MDXLayout ? ( 25 | 26 | <_createMdxContent {...props} /> 27 | 28 | ) : ( 29 | _createMdxContent(props) 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /fixtures/query-preserve-jsx/input.md: -------------------------------------------------------------------------------- 1 | ![](image.png?size=16) 2 | 3 | 4 | -------------------------------------------------------------------------------- /fixtures/query-preserve-jsx/options.json: -------------------------------------------------------------------------------- 1 | { "preserveQuery": "jsx" } 2 | -------------------------------------------------------------------------------- /fixtures/query-preserve-none/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from './image.png' 4 | function _createMdxContent(props) { 5 | const _components = { 6 | img: 'img', 7 | p: 'p', 8 | ...props.components 9 | } 10 | return ( 11 | <> 12 | <_components.p> 13 | <_components.img src={_rehypeMdxImportMedia0} alt="" /> 14 | 15 | {'\n'} 16 | <_components.img srcSet={`${_rehypeMdxImportMedia0} 2x,${_rehypeMdxImportMedia0} 4x`} /> 17 | 18 | ) 19 | } 20 | export default function MDXContent(props = {}) { 21 | const { wrapper: MDXLayout } = props.components || {} 22 | return MDXLayout ? ( 23 | 24 | <_createMdxContent {...props} /> 25 | 26 | ) : ( 27 | _createMdxContent(props) 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /fixtures/query-preserve-none/input.md: -------------------------------------------------------------------------------- 1 | ![](image.png?size=16) 2 | 3 | 4 | -------------------------------------------------------------------------------- /fixtures/query-preserve-none/options.json: -------------------------------------------------------------------------------- 1 | { "preserveQuery": "none" } 2 | -------------------------------------------------------------------------------- /fixtures/relative-grandparent/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from '../../image.png' 4 | function _createMdxContent(props) { 5 | const _components = { 6 | img: 'img', 7 | p: 'p', 8 | ...props.components 9 | } 10 | return ( 11 | <_components.p> 12 | <_components.img src={_rehypeMdxImportMedia0} alt="" /> 13 | 14 | ) 15 | } 16 | export default function MDXContent(props = {}) { 17 | const { wrapper: MDXLayout } = props.components || {} 18 | return MDXLayout ? ( 19 | 20 | <_createMdxContent {...props} /> 21 | 22 | ) : ( 23 | _createMdxContent(props) 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /fixtures/relative-grandparent/input.md: -------------------------------------------------------------------------------- 1 | ![](../../image.png) 2 | -------------------------------------------------------------------------------- /fixtures/relative-parent/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from '../image.png' 4 | function _createMdxContent(props) { 5 | const _components = { 6 | img: 'img', 7 | p: 'p', 8 | ...props.components 9 | } 10 | return ( 11 | <_components.p> 12 | <_components.img src={_rehypeMdxImportMedia0} alt="" /> 13 | 14 | ) 15 | } 16 | export default function MDXContent(props = {}) { 17 | const { wrapper: MDXLayout } = props.components || {} 18 | return MDXLayout ? ( 19 | 20 | <_createMdxContent {...props} /> 21 | 22 | ) : ( 23 | _createMdxContent(props) 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /fixtures/relative-parent/input.md: -------------------------------------------------------------------------------- 1 | ![](../image.png) 2 | -------------------------------------------------------------------------------- /fixtures/relative-sibling/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from './image.png' 4 | function _createMdxContent(props) { 5 | const _components = { 6 | img: 'img', 7 | p: 'p', 8 | ...props.components 9 | } 10 | return ( 11 | <_components.p> 12 | <_components.img src={_rehypeMdxImportMedia0} alt="" /> 13 | 14 | ) 15 | } 16 | export default function MDXContent(props = {}) { 17 | const { wrapper: MDXLayout } = props.components || {} 18 | return MDXLayout ? ( 19 | 20 | <_createMdxContent {...props} /> 21 | 22 | ) : ( 23 | _createMdxContent(props) 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /fixtures/relative-sibling/input.md: -------------------------------------------------------------------------------- 1 | ![](./image.png) 2 | -------------------------------------------------------------------------------- /fixtures/relative-simple/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from './image.png' 4 | function _createMdxContent(props) { 5 | const _components = { 6 | img: 'img', 7 | p: 'p', 8 | ...props.components 9 | } 10 | return ( 11 | <_components.p> 12 | <_components.img src={_rehypeMdxImportMedia0} alt="" /> 13 | 14 | ) 15 | } 16 | export default function MDXContent(props = {}) { 17 | const { wrapper: MDXLayout } = props.components || {} 18 | return MDXLayout ? ( 19 | 20 | <_createMdxContent {...props} /> 21 | 22 | ) : ( 23 | _createMdxContent(props) 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /fixtures/relative-simple/input.md: -------------------------------------------------------------------------------- 1 | ![](image.png) 2 | -------------------------------------------------------------------------------- /fixtures/root/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | function _createMdxContent(props) { 4 | const _components = { 5 | img: 'img', 6 | p: 'p', 7 | ...props.components 8 | } 9 | return ( 10 | <_components.p> 11 | <_components.img src="/image.png" alt="" /> 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/root/input.md: -------------------------------------------------------------------------------- 1 | ![](/image.png) 2 | -------------------------------------------------------------------------------- /fixtures/srcset/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from './image.png' 4 | import _rehypeMdxImportMedia1 from './other-image.png' 5 | import _rehypeMdxImportMedia2 from './sound.mp3' 6 | import _rehypeMdxImportMedia3 from './video.mp4' 7 | import _rehypeMdxImportMedia4 from './image.jpg' 8 | import _rehypeMdxImportMedia5 from './video.webm' 9 | import _rehypeMdxImportMedia6 from './video.mpg' 10 | import _rehypeMdxImportMedia7 from './video.png' 11 | import _rehypeMdxImportMedia8 from './video.vtt' 12 | function _createMdxContent(props) { 13 | const _components = { 14 | audio: 'audio', 15 | embed: 'embed', 16 | img: 'img', 17 | object: 'object', 18 | p: 'p', 19 | picture: 'picture', 20 | source: 'source', 21 | track: 'track', 22 | video: 'video', 23 | ...props.components 24 | } 25 | return ( 26 | <> 27 | <_components.img 28 | srcSet={`${_rehypeMdxImportMedia0} 2x,${_rehypeMdxImportMedia0} 640w 480h,${_rehypeMdxImportMedia1}`} 29 | /> 30 | {'\n'} 31 | <_components.img 32 | srcSet={`${_rehypeMdxImportMedia0} 2x,${_rehypeMdxImportMedia0},${_rehypeMdxImportMedia1} 640w 480h`} 33 | /> 34 | {'\n'} 35 | <_components.img srcSet="https://example.com/image.png" /> 36 | {'\n'} 37 | <_components.p> 38 | <_components.audio src={_rehypeMdxImportMedia2} /> 39 | 40 | {'\n'} 41 | <_components.p> 42 | <_components.embed src={_rehypeMdxImportMedia3} type="video/webm" /> 43 | 44 | {'\n'} 45 | <_components.p> 46 | <_components.object src="video.pdf" type="application/pdf" /> 47 | 48 | {'\n'} 49 | <_components.picture> 50 | {'\n '} 51 | <_components.source srcSet={`${_rehypeMdxImportMedia0}`} /> 52 | {'\n '} 53 | <_components.source srcSet={`${_rehypeMdxImportMedia4}`} /> 54 | {'\n '} 55 | <_components.img src={_rehypeMdxImportMedia4} /> 56 | {'\n'} 57 | 58 | {'\n'} 59 | <_components.video> 60 | {'\n '} 61 | <_components.source src={_rehypeMdxImportMedia5} type="video/webm" /> 62 | {'\n '} 63 | <_components.source src={_rehypeMdxImportMedia6} type="video/mp4" /> 64 | {'\n'} 65 | 66 | {'\n'} 67 | <_components.video src={_rehypeMdxImportMedia5} poster={_rehypeMdxImportMedia7}> 68 | {'\n '} 69 | <_components.track kind="captions" srcLang="en" src={_rehypeMdxImportMedia8} /> 70 | {'\n'} 71 | 72 | 73 | ) 74 | } 75 | export default function MDXContent(props = {}) { 76 | const { wrapper: MDXLayout } = props.components || {} 77 | return MDXLayout ? ( 78 | 79 | <_createMdxContent {...props} /> 80 | 81 | ) : ( 82 | _createMdxContent(props) 83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /fixtures/srcset/input.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 24 | 27 | -------------------------------------------------------------------------------- /fixtures/title/expected.jsx: -------------------------------------------------------------------------------- 1 | /*@jsxRuntime automatic*/ 2 | /*@jsxImportSource react*/ 3 | import _rehypeMdxImportMedia0 from './image.png' 4 | function _createMdxContent(props) { 5 | const _components = { 6 | img: 'img', 7 | p: 'p', 8 | ...props.components 9 | } 10 | return ( 11 | <_components.p> 12 | <_components.img src={_rehypeMdxImportMedia0} alt="" title="Title" /> 13 | 14 | ) 15 | } 16 | export default function MDXContent(props = {}) { 17 | const { wrapper: MDXLayout } = props.components || {} 18 | return MDXLayout ? ( 19 | 20 | <_createMdxContent {...props} /> 21 | 22 | ) : ( 23 | _createMdxContent(props) 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /fixtures/title/input.md: -------------------------------------------------------------------------------- 1 | ![](image.png 'Title') 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rehype-mdx-import-media", 3 | "version": "1.2.0", 4 | "description": "An MDX rehype plugin for turning media paths into imports.", 5 | "type": "module", 6 | "exports": "./dist/rehype-mdx-import-media.js", 7 | "files": [ 8 | "src", 9 | "dist", 10 | "!*.test.*" 11 | ], 12 | "scripts": { 13 | "prepack": "tsc --build", 14 | "pretest": "tsc --build", 15 | "test": "c8 node --enable-source-maps --test" 16 | }, 17 | "author": "Remco Haszing ", 18 | "license": "MIT", 19 | "repository": "remcohaszing/rehype-mdx-import-media", 20 | "bugs": "https://github.com/remcohaszing/rehype-mdx-import-media/issues", 21 | "homepage": "https://github.com/remcohaszing/rehype-mdx-import-media#readme", 22 | "keywords": [ 23 | "hast", 24 | "mdx", 25 | "rehype", 26 | "rehype-plugin", 27 | "unified" 28 | ], 29 | "dependencies": { 30 | "@types/hast": "^3.0.0", 31 | "hast-util-properties-to-mdx-jsx-attributes": "^1.0.0", 32 | "parse-srcset": "^1.0.0", 33 | "unified": "^11.0.0", 34 | "unist-util-visit": "^5.0.0" 35 | }, 36 | "devDependencies": { 37 | "@mdx-js/mdx": "^3.0.0", 38 | "@types/estree": "^1.0.0", 39 | "@types/node": "^20.0.0", 40 | "@types/parse-srcset": "^1.0.0", 41 | "c8": "^9.0.0", 42 | "eslint": "^8.0.0", 43 | "eslint-config-remcohaszing": "^10.0.0", 44 | "eslint-plugin-jsx-a11y": "^6.0.0", 45 | "eslint-plugin-react": "^7.0.0", 46 | "prettier": "^3.0.0", 47 | "rehype-raw": "^7.0.0", 48 | "remark-cli": "^11.0.0", 49 | "remark-preset-remcohaszing": "^2.0.0", 50 | "snapshot-fixtures": "^1.0.0", 51 | "typescript": "^5.0.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/rehype-mdx-import-media.test.ts: -------------------------------------------------------------------------------- 1 | import { compileSync } from '@mdx-js/mdx' 2 | import rehypeMdxImportMedia, { type RehypeMdxImportMediaOptions } from 'rehype-mdx-import-media' 3 | import rehypeRaw from 'rehype-raw' 4 | import { testFixturesDirectory } from 'snapshot-fixtures' 5 | 6 | testFixturesDirectory({ 7 | directory: new URL('../fixtures', import.meta.url), 8 | prettier: true, 9 | tests: { 10 | 'expected.jsx'(file, options) { 11 | return compileSync(file, { 12 | rehypePlugins: [rehypeRaw, [rehypeMdxImportMedia, options]], 13 | jsx: true 14 | }) 15 | } 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /src/rehype-mdx-import-media.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Expression, 3 | type Identifier, 4 | type ImportDeclaration, 5 | type TemplateElement 6 | } from 'estree' 7 | import { type Root } from 'hast' 8 | import { propertiesToMdxJsxAttributes } from 'hast-util-properties-to-mdx-jsx-attributes' 9 | import parseSrcset from 'parse-srcset' 10 | import { type Plugin } from 'unified' 11 | import { visit } from 'unist-util-visit' 12 | 13 | export interface RehypeMdxImportMediaOptions { 14 | /** 15 | * HTML element attributes that should be processed. The key is the HTML element tag name. The 16 | * value is a list of attribute names to process. The default value is {@link defaultAttributes} 17 | */ 18 | attributes?: Record> 19 | 20 | /** 21 | * The casing to use for attribute names. 22 | * 23 | * This should match the `elementAttributeNameCase` value passed to MDX. 24 | * 25 | * @default 'react' 26 | * @see https://mdxjs.com/packages/mdx/#processoroptions 27 | */ 28 | elementAttributeNameCase?: 'html' | 'react' 29 | 30 | /** 31 | * Where to keep URL hash. 32 | * 33 | * - `both`: Keep the URL hash on both the import source and the JSX prop. 34 | * - `import`: Only keep the URL hash on the import source. 35 | * - `jsx`: Only keep the URL hash on the JSX prop. 36 | * - `none`: Remove the URL hash. 37 | * 38 | * @default 'import' 39 | */ 40 | preserveHash?: 'both' | 'import' | 'jsx' | 'none' 41 | 42 | /** 43 | * Where to keep query parameters. 44 | * 45 | * - `both`: Keep the query parameters on both the import source and the JSX prop. 46 | * - `import`: Only keep the query parameters on the import source. 47 | * - `jsx`: Only keep the query parameters on the JSX prop. 48 | * - `none`: Remove the query parameters. 49 | * 50 | * @default 'import' 51 | */ 52 | preserveQuery?: 'both' | 'import' | 'jsx' | 'none' 53 | 54 | /** 55 | * By default imports are resolved relative to the input file. This matches default markdown 56 | * behaviour. If this is set to false, this behaviour is removed and URLs are no longer processed. 57 | * This allows to import images from `node_modules`. If this is disabled, local images can still 58 | * be imported by prepending the path with `./`. 59 | * 60 | * @default true 61 | */ 62 | resolve?: boolean 63 | } 64 | 65 | const urlPattern = /^(https?:)?\// 66 | const relativePathPattern = /\.\.?\// 67 | 68 | export const defaultAttributes: Record> = { 69 | audio: 'src', 70 | embed: 'src', 71 | img: ['src', 'srcset'], 72 | object: 'data', 73 | source: ['src', 'srcset'], 74 | track: 'src', 75 | video: ['poster', 'src'] 76 | } 77 | 78 | /** 79 | * A rehype MDX plugin for converting media sources into imports. 80 | */ 81 | const rehypeMdxImportMedia: Plugin<[RehypeMdxImportMediaOptions?], Root> = ({ 82 | attributes = defaultAttributes, 83 | elementAttributeNameCase, 84 | preserveHash = 'import', 85 | preserveQuery = 'import', 86 | resolve = true 87 | } = {}) => { 88 | const elementMap = new Map( 89 | Object.entries(attributes).map(([tagName, attributeNames]) => { 90 | const set = new Set() 91 | if (typeof attributeNames === 'string') { 92 | set.add(attributeNames.toLowerCase()) 93 | } else { 94 | for (const name of attributeNames) { 95 | set.add(name.toLowerCase()) 96 | } 97 | } 98 | return [tagName.toLowerCase(), set] 99 | }) 100 | ) 101 | 102 | return (ast) => { 103 | const imports: ImportDeclaration[] = [] 104 | const imported = new Map() 105 | 106 | visit(ast, 'element', (node, index, parent) => { 107 | const attributeNames = elementMap.get(node.tagName) 108 | if (!attributeNames) { 109 | return 110 | } 111 | 112 | let shouldReplace = false 113 | 114 | // Don’t even bother continuing if there are no properties to replace. 115 | for (const name in node.properties) { 116 | if (attributeNames.has(name.toLowerCase())) { 117 | shouldReplace = true 118 | break 119 | } 120 | } 121 | 122 | if (!shouldReplace) { 123 | return 124 | } 125 | 126 | shouldReplace = false 127 | 128 | /** 129 | * Generate an identifier node for an import path. 130 | * 131 | * If the path should not be replaced, nothing is returned. If an identifier was already 132 | * calculated for this path, it is returned instead. 133 | * 134 | * @param importSource 135 | * The path to get an identifier for. 136 | * @returns 137 | * The matching identifier, or none. 138 | */ 139 | function getIdentifier(importSource: string): [] | [Identifier, string] { 140 | let value = importSource 141 | if (urlPattern.test(value)) { 142 | return [] 143 | } 144 | 145 | if (!relativePathPattern.test(value) && resolve) { 146 | value = `./${value}` 147 | } 148 | 149 | const hashIndex = value.indexOf('#') 150 | const hash = hashIndex === -1 ? '' : value.slice(hashIndex) 151 | const remainder = hashIndex === -1 ? value : value.slice(0, hashIndex) 152 | const queryIndex = remainder.indexOf('?') 153 | const query = queryIndex === -1 ? '' : remainder.slice(queryIndex) 154 | value = queryIndex === -1 ? remainder : remainder.slice(0, queryIndex) 155 | let propChunk = '' 156 | 157 | if (preserveQuery === 'import') { 158 | value += query 159 | } else if (preserveQuery === 'jsx') { 160 | propChunk += query 161 | } else if (preserveQuery === 'both') { 162 | value += query 163 | propChunk += query 164 | } 165 | 166 | if (preserveHash === 'import') { 167 | value += hash 168 | } else if (preserveHash === 'jsx') { 169 | propChunk += hash 170 | } else if (preserveHash === 'both') { 171 | value += hash 172 | propChunk += hash 173 | } 174 | 175 | let name = imported.get(value) 176 | 177 | if (!name) { 178 | name = `_rehypeMdxImportMedia${imported.size}` 179 | 180 | imports.push({ 181 | type: 'ImportDeclaration', 182 | source: { type: 'Literal', value }, 183 | specifiers: [{ type: 'ImportDefaultSpecifier', local: { type: 'Identifier', name } }] 184 | }) 185 | imported.set(value, name) 186 | } 187 | 188 | shouldReplace = true 189 | return [{ type: 'Identifier', name }, propChunk] 190 | } 191 | 192 | const replacements = propertiesToMdxJsxAttributes(node.properties, { 193 | elementAttributeNameCase, 194 | transform(name, value) { 195 | if (!value) { 196 | return value 197 | } 198 | 199 | const lower = name.toLowerCase() 200 | if (!attributeNames.has(lower)) { 201 | return value 202 | } 203 | 204 | if (lower !== 'srcset') { 205 | const [identifier, extra] = getIdentifier(value) 206 | 207 | if (!identifier) { 208 | return value 209 | } 210 | 211 | if (extra) { 212 | return { 213 | type: 'TemplateLiteral', 214 | expressions: [identifier], 215 | quasis: [ 216 | { type: 'TemplateElement', tail: false, value: { raw: '' } }, 217 | { type: 'TemplateElement', tail: true, value: { raw: extra } } 218 | ] 219 | } 220 | } 221 | 222 | return identifier 223 | } 224 | 225 | const srcset = parseSrcset(value) 226 | const expressions: Expression[] = [] 227 | const quasis: TemplateElement[] = [] 228 | let raw = '' 229 | 230 | for (const [srcIndex, src] of srcset.entries()) { 231 | const [identifier, extra] = getIdentifier(src.url) 232 | 233 | if (identifier) { 234 | quasis.push({ type: 'TemplateElement', tail: false, value: { raw } }) 235 | expressions.push(identifier) 236 | raw = extra! 237 | } else { 238 | raw += src.url 239 | } 240 | 241 | if (src.d) { 242 | raw += ` ${src.d}x` 243 | } 244 | 245 | if (src.w) { 246 | raw += ` ${src.w}w` 247 | } 248 | 249 | if (src.h) { 250 | raw += ` ${src.h}h` 251 | } 252 | 253 | if (srcIndex < srcset.length - 1) { 254 | raw += ',' 255 | } 256 | } 257 | 258 | if (!expressions.length) { 259 | return value 260 | } 261 | 262 | quasis.push({ type: 'TemplateElement', tail: true, value: { raw } }) 263 | return { type: 'TemplateLiteral', expressions, quasis } 264 | } 265 | }) 266 | 267 | if (shouldReplace) { 268 | parent!.children[index!] = { 269 | type: 'mdxJsxTextElement', 270 | name: node.tagName, 271 | attributes: replacements, 272 | children: node.children, 273 | data: node.data, 274 | position: node.position 275 | } 276 | } 277 | }) 278 | 279 | if (imports.length) { 280 | ast.children.unshift({ 281 | type: 'mdxjsEsm', 282 | value: '', 283 | data: { 284 | estree: { 285 | type: 'Program', 286 | sourceType: 'module', 287 | body: imports 288 | } 289 | } 290 | }) 291 | } 292 | } 293 | } 294 | 295 | export default rehypeMdxImportMedia 296 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "declaration": true, 5 | "declarationMap": true, 6 | "lib": ["es2022"], 7 | "module": "nodenext", 8 | "outDir": "dist", 9 | "rootDir": "src", 10 | "skipLibCheck": true, 11 | "sourceMap": true, 12 | "strict": true, 13 | "target": "es2022" 14 | } 15 | } 16 | --------------------------------------------------------------------------------