Skip to content

Commit

Permalink
fix(mdx): convert remark-images-to-component plugin to a rehype plugin (
Browse files Browse the repository at this point in the history
  • Loading branch information
ArmandPhilippot authored Apr 24, 2024
1 parent a940465 commit 20936a9
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 160 deletions.
5 changes: 5 additions & 0 deletions .changeset/sweet-goats-own.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions packages/integrations/mdx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,15 @@
},
"devDependencies": {
"@types/estree": "^1.0.5",
"@types/hast": "^3.0.3",
"@types/mdast": "^4.0.3",
"@types/yargs-parser": "^21.0.3",
"astro": "workspace:*",
"astro-scripts": "workspace:*",
"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",
Expand Down
6 changes: 3 additions & 3 deletions packages/integrations/mdx/src/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -52,7 +52,7 @@ function getRemarkPlugins(mdxOptions: MdxOptions): PluggableList {
}
}

remarkPlugins.push(...mdxOptions.remarkPlugins, remarkCollectImages, remarkImageToComponent);
remarkPlugins.push(...mdxOptions.remarkPlugins, remarkCollectImages);

return remarkPlugins;
}
Expand All @@ -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.
Expand Down
166 changes: 166 additions & 0 deletions packages/integrations/mdx/src/rehype-images-to-component.ts
Original file line number Diff line number Diff line change
@@ -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 <img /> 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;

/*
* <Image /> 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<string, string>();

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 <Image src={importName} {...attributes} />
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`));
};
}
156 changes: 0 additions & 156 deletions packages/integrations/mdx/src/remark-images-to-component.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 20936a9

Please sign in to comment.