diff --git a/README.md b/README.md index d0a6f28..148df77 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,17 @@ behaviour. If this is set to false, this behaviour is removed and URLs are no lo allows to import images from `node_modules`. If this is disabled, local images can still be imported by prepending the path with `./`. +### `includeDimensions` + +It can be very useful to have the dimensions of images associated to the img tag. Including the image +size can avoid content shift. Toggle this option on to add height/width attributes to your tags when the images +are imported via relative import. You will also need to set `cwd` option for this to work + +### `cwd` + +The current working directory of the file being parsed. Used in combination with `includeDimensions` to load images +with their specific width/height attributes. + ### License [MIT](LICENSE.md) @ [Remco Haszing](https://github.com/remcohaszing) diff --git a/__fixtures__/size/expected.jsx b/__fixtures__/size/expected.jsx new file mode 100644 index 0000000..d25ddc8 --- /dev/null +++ b/__fixtures__/size/expected.jsx @@ -0,0 +1,32 @@ +/*@jsxRuntime automatic @jsxImportSource react*/ +import __0___image_png__ from './image.png'; +function _createMdxContent(props) { + const _components = Object.assign( + { + p: 'p', + img: 'img', + }, + props.components, + ); + return ( + <_components.p> + <_components.img + alt="Alt text" + src={__0___image_png__} + width="512" + height="512" + /> + + ); +} +function MDXContent(props = {}) { + const { wrapper: MDXLayout } = props.components || {}; + return MDXLayout ? ( + + <_createMdxContent {...props} /> + + ) : ( + _createMdxContent(props) + ); +} +export default MDXContent; diff --git a/__fixtures__/size/image.png b/__fixtures__/size/image.png new file mode 100644 index 0000000..9490ffc Binary files /dev/null and b/__fixtures__/size/image.png differ diff --git a/__fixtures__/size/input.md b/__fixtures__/size/input.md new file mode 100644 index 0000000..806b3be --- /dev/null +++ b/__fixtures__/size/input.md @@ -0,0 +1 @@ +![Alt text](./image.png) diff --git a/__fixtures__/size/options.json b/__fixtures__/size/options.json new file mode 100644 index 0000000..344bcd8 --- /dev/null +++ b/__fixtures__/size/options.json @@ -0,0 +1,4 @@ +{ + "includeDimensions": true, + "imagesDirectory": "__fixtures__/size" +} diff --git a/index.ts b/index.ts index 679656e..d7ae53d 100644 --- a/index.ts +++ b/index.ts @@ -1,3 +1,6 @@ +import { join } from 'path'; + +import { imageSize } from 'image-size'; import { Image, Parent, Root } from 'mdast'; import { MdxjsEsm, MdxJsxTextElement } from 'mdast-util-mdx'; import { Plugin } from 'unified'; @@ -13,20 +16,61 @@ export interface RemarkMdxImagesOptions { * @default true */ resolve?: boolean; + /** + * Adds the width and height attributes to the image tag. + * + * @default false + */ + includeDimensions?: boolean; + /** + * The current working directory of the file being parsed. Used in combination with + * `includeDimensions` to load images with their specific width/height attributes. + */ + cwd?: string; } // eslint-disable-next-line unicorn/no-unsafe-regex const urlPattern = /^(https?:)?\//; const relativePathPattern = /\.\.?\//; +// eslint-disable-next-line unicorn/no-unsafe-regex +const absolutePathRegex = /^(?:[a-z]+:)?\/\//; + +/** + * Gets the size of the image. + * + * @param src The image source. + * @returns the image dimensions + */ +function getImageSize(src: string): + | { + /** + * Width of the image. + */ + width?: number; + /** + * Height of the image. + */ + height?: number; + } + | undefined { + if (absolutePathRegex.test(src)) { + return undefined; + } + + return imageSize(src); +} /** * A Remark plugin for converting Markdown images to MDX images using imports for the image source. */ const remarkMdxImages: Plugin<[RemarkMdxImagesOptions?], Root> = - ({ resolve = true } = {}) => + ({ cwd, includeDimensions = false, resolve = true } = {}) => (ast) => { const imports: MdxjsEsm[] = []; const imported = new Map(); + if (includeDimensions && !cwd) { + throw new Error('The cwd option is required when includeDimensions is enabled.'); + } visit(ast, 'image', (node: Image, index: number | null, parent: Parent | null) => { let { alt = null, title, url } = node; @@ -38,6 +82,8 @@ const remarkMdxImages: Plugin<[RemarkMdxImagesOptions?], Root> = } let name = imported.get(url); + const size = + includeDimensions && imagesDirectory ? getImageSize(join(imagesDirectory, url)) : undefined; if (!name) { name = `__${imported.size}_${url.replace(/\W/g, '_')}__`; @@ -93,6 +139,20 @@ const remarkMdxImages: Plugin<[RemarkMdxImagesOptions?], Root> = if (title) { textElement.attributes.push({ type: 'mdxJsxAttribute', name: 'title', value: title }); } + if (size?.width) { + textElement.attributes.push({ + type: 'mdxJsxAttribute', + name: 'width', + value: String(size.width), + }); + } + if (size?.height) { + textElement.attributes.push({ + type: 'mdxJsxAttribute', + name: 'height', + value: String(size.height), + }); + } parent!.children.splice(index!, 1, textElement); }); ast.children.unshift(...imports); diff --git a/package-lock.json b/package-lock.json index 706114c..b557134 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,13 @@ "version": "2.0.0", "license": "MIT", "dependencies": { + "@types/mdast": "^3.0.0", + "image-size": "^1.0.2", + "unified": "^10.0.0", "unist-util-visit": "^4.0.0" }, "devDependencies": { "@mdx-js/mdx": "^2.0.0", - "@types/mdast": "^3.0.0", "@types/node": "^18.0.0", "@types/prettier": "^2.0.0", "@typescript-eslint/eslint-plugin": "^5.0.0", @@ -26,11 +28,10 @@ "prettier": "^2.0.0", "tsx": "^3.0.0", "typescript": "^4.0.0", - "unified": "^10.0.0", "uvu": "^0.5.0" }, "engines": { - "node": ">=12.2.0" + "node": ">=14.0.0" } }, "node_modules/@babel/code-frame": { @@ -441,7 +442,6 @@ "version": "3.0.10", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz", "integrity": "sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==", - "dev": true, "dependencies": { "@types/unist": "*" } @@ -852,7 +852,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "dev": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -2507,8 +2506,7 @@ "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -2945,6 +2943,20 @@ "node": ">= 4" } }, + "node_modules/image-size": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.2.tgz", + "integrity": "sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -2993,8 +3005,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/inline-style-parser": { "version": "0.1.1", @@ -3081,7 +3092,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "dev": true, "funding": [ { "type": "github", @@ -3244,7 +3254,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "dev": true, "engines": { "node": ">=12" }, @@ -5080,6 +5089,14 @@ "node": ">=6" } }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "dependencies": { + "inherits": "~2.0.3" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5857,7 +5874,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==", - "dev": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -5970,7 +5986,6 @@ "version": "10.1.2", "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "dev": true, "dependencies": { "@types/unist": "^2.0.0", "bail": "^2.0.0", @@ -6160,7 +6175,6 @@ "version": "5.3.4", "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.4.tgz", "integrity": "sha512-KI+7cnst03KbEyN1+JE504zF5bJBZa+J+CrevLeyIMq0aPU681I2rQ5p4PlnQ6exFtWiUrg26QUdFMnAKR6PIw==", - "dev": true, "dependencies": { "@types/unist": "^2.0.0", "is-buffer": "^2.0.0", @@ -6190,7 +6204,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.2.tgz", "integrity": "sha512-QjSNP6Yxzyycd4SVOtmKKyTsSvClqBPJcd00Z0zuPj3hOIjg0rUPG6DbFGPvUKRgYyaIWLPKpuEclcuvb3H8qA==", - "dev": true, "dependencies": { "@types/unist": "^2.0.0", "unist-util-stringify-position": "^3.0.0" @@ -6204,7 +6217,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.2.tgz", "integrity": "sha512-7A6eiDCs9UtjcwZOcCpM4aPII3bAAGv13E96IkawkOAW0OhH+yRxtY0lzo8KiHpzEMfH7Q+FizUmwp8Iqy5EWg==", - "dev": true, "dependencies": { "@types/unist": "^2.0.0" }, @@ -6217,7 +6229,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.2.tgz", "integrity": "sha512-7A6eiDCs9UtjcwZOcCpM4aPII3bAAGv13E96IkawkOAW0OhH+yRxtY0lzo8KiHpzEMfH7Q+FizUmwp8Iqy5EWg==", - "dev": true, "dependencies": { "@types/unist": "^2.0.0" }, diff --git a/package.json b/package.json index a9a8fb0..ed84802 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ }, "dependencies": { "@types/mdast": "^3.0.0", + "image-size": "^1.0.2", "unified": "^10.0.0", "unist-util-visit": "^4.0.0" },