diff --git a/.changeset/sweet-goats-own.md b/.changeset/sweet-goats-own.md
new file mode 100644
index 000000000000..172376c29564
--- /dev/null
+++ b/.changeset/sweet-goats-own.md
@@ -0,0 +1,5 @@
+---
+"@astrojs/mdx": major
+---
+
+Replace remark-images-to-component with rehype-images-to-component to let users use additional rehype plugins for images
diff --git a/packages/integrations/mdx/package.json b/packages/integrations/mdx/package.json
index cb3016167498..e1055cb52318 100644
--- a/packages/integrations/mdx/package.json
+++ b/packages/integrations/mdx/package.json
@@ -54,6 +54,7 @@
},
"devDependencies": {
"@types/estree": "^1.0.5",
+ "@types/hast": "^3.0.3",
"@types/mdast": "^4.0.3",
"@types/yargs-parser": "^21.0.3",
"astro": "workspace:*",
@@ -61,6 +62,7 @@
"cheerio": "1.0.0-rc.12",
"linkedom": "^0.16.11",
"mdast-util-mdx": "^3.0.0",
+ "mdast-util-mdx-jsx": "^3.1.2",
"mdast-util-to-string": "^4.0.0",
"reading-time": "^1.5.0",
"rehype-mathjax": "^6.0.0",
diff --git a/packages/integrations/mdx/src/plugins.ts b/packages/integrations/mdx/src/plugins.ts
index 99d0c70b2756..e314be1893a9 100644
--- a/packages/integrations/mdx/src/plugins.ts
+++ b/packages/integrations/mdx/src/plugins.ts
@@ -15,7 +15,7 @@ import { rehypeApplyFrontmatterExport } from './rehype-apply-frontmatter-export.
import { rehypeInjectHeadingsExport } from './rehype-collect-headings.js';
import rehypeMetaString from './rehype-meta-string.js';
import { rehypeOptimizeStatic } from './rehype-optimize-static.js';
-import { remarkImageToComponent } from './remark-images-to-component.js';
+import { rehypeImageToComponent } from './rehype-images-to-component.js';
// Skip nonessential plugins during performance benchmark runs
const isPerformanceBenchmark = Boolean(process.env.ASTRO_PERFORMANCE_BENCHMARK);
@@ -52,7 +52,7 @@ function getRemarkPlugins(mdxOptions: MdxOptions): PluggableList {
}
}
- remarkPlugins.push(...mdxOptions.remarkPlugins, remarkCollectImages, remarkImageToComponent);
+ remarkPlugins.push(...mdxOptions.remarkPlugins, remarkCollectImages);
return remarkPlugins;
}
@@ -74,7 +74,7 @@ function getRehypePlugins(mdxOptions: MdxOptions): PluggableList {
}
}
- rehypePlugins.push(...mdxOptions.rehypePlugins);
+ rehypePlugins.push(...mdxOptions.rehypePlugins, rehypeImageToComponent);
if (!isPerformanceBenchmark) {
// getHeadings() is guaranteed by TS, so this must be included.
diff --git a/packages/integrations/mdx/src/rehype-images-to-component.ts b/packages/integrations/mdx/src/rehype-images-to-component.ts
new file mode 100644
index 000000000000..6c797fda235f
--- /dev/null
+++ b/packages/integrations/mdx/src/rehype-images-to-component.ts
@@ -0,0 +1,166 @@
+import type { MarkdownVFile } from '@astrojs/markdown-remark';
+import type { Properties, Root } from 'hast';
+import type { MdxJsxAttribute, MdxjsEsm } from 'mdast-util-mdx';
+import type { MdxJsxFlowElementHast } from 'mdast-util-mdx-jsx';
+import { visit } from 'unist-util-visit';
+import { jsToTreeNode } from './utils.js';
+
+export const ASTRO_IMAGE_ELEMENT = 'astro-image';
+export const ASTRO_IMAGE_IMPORT = '__AstroImage__';
+export const USES_ASTRO_IMAGE_FLAG = '__usesAstroImage';
+
+function createArrayAttribute(name: string, values: (string | number)[]): MdxJsxAttribute {
+ return {
+ type: 'mdxJsxAttribute',
+ name: name,
+ value: {
+ type: 'mdxJsxAttributeValueExpression',
+ value: name,
+ data: {
+ estree: {
+ type: 'Program',
+ body: [
+ {
+ type: 'ExpressionStatement',
+ expression: {
+ type: 'ArrayExpression',
+ elements: values.map((value) => ({
+ type: 'Literal',
+ value: value,
+ raw: String(value),
+ })),
+ },
+ },
+ ],
+ sourceType: 'module',
+ comments: [],
+ },
+ },
+ },
+ };
+}
+
+/**
+ * Convert the element properties (except `src`) to MDX JSX attributes.
+ *
+ * @param {Properties} props - The element properties
+ * @returns {MdxJsxAttribute[]} The MDX attributes
+ */
+function getImageComponentAttributes(props: Properties): MdxJsxAttribute[] {
+ const attrs: MdxJsxAttribute[] = [];
+
+ for (const [prop, value] of Object.entries(props)) {
+ if (prop === 'src') continue;
+
+ /*
+ * component expects an array for those attributes but the
+ * received properties are sanitized as strings. So we need to convert them
+ * back to an array.
+ */
+ if (prop === 'widths' || prop === 'densities') {
+ attrs.push(createArrayAttribute(prop, String(value).split(' ')));
+ } else {
+ attrs.push({
+ name: prop,
+ type: 'mdxJsxAttribute',
+ value: String(value),
+ });
+ }
+ }
+
+ return attrs;
+}
+
+export function rehypeImageToComponent() {
+ return function (tree: Root, file: MarkdownVFile) {
+ if (!file.data.imagePaths) return;
+
+ const importsStatements: MdxjsEsm[] = [];
+ const importedImages = new Map();
+
+ visit(tree, 'element', (node, index, parent) => {
+ if (!file.data.imagePaths || node.tagName !== 'img' || !node.properties.src) return;
+
+ const src = decodeURI(String(node.properties.src));
+
+ if (!file.data.imagePaths.has(src)) return;
+
+ let importName = importedImages.get(src);
+
+ if (!importName) {
+ importName = `__${importedImages.size}_${src.replace(/\W/g, '_')}__`;
+
+ importsStatements.push({
+ type: 'mdxjsEsm',
+ value: '',
+ data: {
+ estree: {
+ type: 'Program',
+ sourceType: 'module',
+ body: [
+ {
+ type: 'ImportDeclaration',
+ source: {
+ type: 'Literal',
+ value: src,
+ raw: JSON.stringify(src),
+ },
+ specifiers: [
+ {
+ type: 'ImportDefaultSpecifier',
+ local: { type: 'Identifier', name: importName },
+ },
+ ],
+ },
+ ],
+ },
+ },
+ });
+ importedImages.set(src, importName);
+ }
+
+ // Build a component that's equivalent to
+ const componentElement: MdxJsxFlowElementHast = {
+ name: ASTRO_IMAGE_ELEMENT,
+ type: 'mdxJsxFlowElement',
+ attributes: [
+ ...getImageComponentAttributes(node.properties),
+ {
+ name: 'src',
+ type: 'mdxJsxAttribute',
+ value: {
+ type: 'mdxJsxAttributeValueExpression',
+ value: importName,
+ data: {
+ estree: {
+ type: 'Program',
+ sourceType: 'module',
+ comments: [],
+ body: [
+ {
+ type: 'ExpressionStatement',
+ expression: { type: 'Identifier', name: importName },
+ },
+ ],
+ },
+ },
+ },
+ },
+ ],
+ children: [],
+ };
+
+ parent!.children.splice(index!, 1, componentElement);
+ });
+
+ // Add all the import statements to the top of the file for the images
+ tree.children.unshift(...importsStatements);
+
+ tree.children.unshift(
+ jsToTreeNode(`import { Image as ${ASTRO_IMAGE_IMPORT} } from "astro:assets";`)
+ );
+ // Export `__usesAstroImage` to pick up `astro:assets` usage in the module graph.
+ // @see the '@astrojs/mdx-postprocess' plugin
+ tree.children.push(jsToTreeNode(`export const ${USES_ASTRO_IMAGE_FLAG} = true`));
+ };
+}
diff --git a/packages/integrations/mdx/src/remark-images-to-component.ts b/packages/integrations/mdx/src/remark-images-to-component.ts
deleted file mode 100644
index 46d04d443341..000000000000
--- a/packages/integrations/mdx/src/remark-images-to-component.ts
+++ /dev/null
@@ -1,156 +0,0 @@
-import type { MarkdownVFile } from '@astrojs/markdown-remark';
-import type { Image, Parent } from 'mdast';
-import type { MdxJsxAttribute, MdxJsxFlowElement, MdxjsEsm } from 'mdast-util-mdx';
-import { visit } from 'unist-util-visit';
-import { jsToTreeNode } from './utils.js';
-
-export const ASTRO_IMAGE_ELEMENT = 'astro-image';
-export const ASTRO_IMAGE_IMPORT = '__AstroImage__';
-export const USES_ASTRO_IMAGE_FLAG = '__usesAstroImage';
-
-export function remarkImageToComponent() {
- return function (tree: any, file: MarkdownVFile) {
- if (!file.data.imagePaths) return;
-
- const importsStatements: MdxjsEsm[] = [];
- const importedImages = new Map();
-
- visit(tree, 'image', (node: Image, index: number | undefined, parent: Parent | null) => {
- // Use the imagePaths set from the remark-collect-images so we don't have to duplicate the logic for
- // checking if an image should be imported or not
- if (file.data.imagePaths?.has(node.url)) {
- let importName = importedImages.get(node.url);
-
- // If we haven't already imported this image, add an import statement
- if (!importName) {
- importName = `__${importedImages.size}_${node.url.replace(/\W/g, '_')}__`;
- importsStatements.push({
- type: 'mdxjsEsm',
- value: '',
- data: {
- estree: {
- type: 'Program',
- sourceType: 'module',
- body: [
- {
- type: 'ImportDeclaration',
- source: {
- type: 'Literal',
- value: node.url,
- raw: JSON.stringify(node.url),
- },
- specifiers: [
- {
- type: 'ImportDefaultSpecifier',
- local: { type: 'Identifier', name: importName },
- },
- ],
- },
- ],
- },
- },
- });
- importedImages.set(node.url, importName);
- }
-
- // Build a component that's equivalent to
- const componentElement: MdxJsxFlowElement = {
- name: ASTRO_IMAGE_ELEMENT,
- type: 'mdxJsxFlowElement',
- attributes: [
- {
- name: 'src',
- type: 'mdxJsxAttribute',
- value: {
- type: 'mdxJsxAttributeValueExpression',
- value: importName,
- data: {
- estree: {
- type: 'Program',
- sourceType: 'module',
- comments: [],
- body: [
- {
- type: 'ExpressionStatement',
- expression: { type: 'Identifier', name: importName },
- },
- ],
- },
- },
- },
- },
- { name: 'alt', type: 'mdxJsxAttribute', value: node.alt || '' },
- ],
- children: [],
- };
-
- if (node.title) {
- componentElement.attributes.push({
- type: 'mdxJsxAttribute',
- name: 'title',
- value: node.title,
- });
- }
-
- if (node.data && node.data.hProperties) {
- const createArrayAttribute = (name: string, values: string[]): MdxJsxAttribute => {
- return {
- type: 'mdxJsxAttribute',
- name: name,
- value: {
- type: 'mdxJsxAttributeValueExpression',
- value: name,
- data: {
- estree: {
- type: 'Program',
- body: [
- {
- type: 'ExpressionStatement',
- expression: {
- type: 'ArrayExpression',
- elements: values.map((value) => ({
- type: 'Literal',
- value: value,
- raw: String(value),
- })),
- },
- },
- ],
- sourceType: 'module',
- comments: [],
- },
- },
- },
- };
- };
- // Go through every hProperty and add it as an attribute of the
- Object.entries(node.data.hProperties as Record).forEach(
- ([key, value]) => {
- if (Array.isArray(value)) {
- componentElement.attributes.push(createArrayAttribute(key, value));
- } else {
- componentElement.attributes.push({
- name: key,
- type: 'mdxJsxAttribute',
- value: String(value),
- });
- }
- }
- );
- }
-
- parent!.children.splice(index!, 1, componentElement);
- }
- });
-
- // Add all the import statements to the top of the file for the images
- tree.children.unshift(...importsStatements);
-
- tree.children.unshift(
- jsToTreeNode(`import { Image as ${ASTRO_IMAGE_IMPORT} } from "astro:assets";`)
- );
- // Export `__usesAstroImage` to pick up `astro:assets` usage in the module graph.
- // @see the '@astrojs/mdx-postprocess' plugin
- tree.children.push(jsToTreeNode(`export const ${USES_ASTRO_IMAGE_FLAG} = true`));
- };
-}
diff --git a/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts b/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts
index c60504be6c9c..0ce1cca24477 100644
--- a/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts
+++ b/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts
@@ -5,7 +5,7 @@ import {
ASTRO_IMAGE_ELEMENT,
ASTRO_IMAGE_IMPORT,
USES_ASTRO_IMAGE_FLAG,
-} from './remark-images-to-component.js';
+} from './rehype-images-to-component.js';
import { type FileInfo, getFileInfo } from './utils.js';
// These transforms must happen *after* JSX runtime transformations
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f4f5d9cb6729..76b7f4547c23 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4411,6 +4411,9 @@ importers:
'@types/estree':
specifier: ^1.0.5
version: 1.0.5
+ '@types/hast':
+ specifier: ^3.0.3
+ version: 3.0.4
'@types/mdast':
specifier: ^4.0.3
version: 4.0.3
@@ -4432,6 +4435,9 @@ importers:
mdast-util-mdx:
specifier: ^3.0.0
version: 3.0.0
+ mdast-util-mdx-jsx:
+ specifier: ^3.1.2
+ version: 3.1.2
mdast-util-to-string:
specifier: ^4.0.0
version: 4.0.0