feat(mdx): Add support for turning ![]() into <Image> (#6824)

This commit is contained in:
Erika 2023-04-13 11:54:40 +02:00 committed by GitHub
parent 948a6d7be0
commit 2511d58d58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 195 additions and 7 deletions

View file

@ -0,0 +1,6 @@
---
'@astrojs/mdx': minor
'@astrojs/markdown-remark': patch
---
Add support for using optimized and relative images in MDX files with `experimental.assets`

View file

@ -1,4 +1,4 @@
import { rehypeHeadingIds } from '@astrojs/markdown-remark'; import { rehypeHeadingIds, remarkCollectImages } from '@astrojs/markdown-remark';
import { import {
InvalidAstroDataError, InvalidAstroDataError,
safelyGetAstroData, safelyGetAstroData,
@ -16,6 +16,7 @@ import type { VFile } from 'vfile';
import type { MdxOptions } from './index.js'; import type { MdxOptions } from './index.js';
import { rehypeInjectHeadingsExport } from './rehype-collect-headings.js'; import { rehypeInjectHeadingsExport } from './rehype-collect-headings.js';
import rehypeMetaString from './rehype-meta-string.js'; import rehypeMetaString from './rehype-meta-string.js';
import { remarkImageToComponent } from './remark-images-to-component.js';
import remarkPrism from './remark-prism.js'; import remarkPrism from './remark-prism.js';
import remarkShiki from './remark-shiki.js'; import remarkShiki from './remark-shiki.js';
import { jsToTreeNode } from './utils.js'; import { jsToTreeNode } from './utils.js';
@ -99,7 +100,7 @@ export async function getRemarkPlugins(
mdxOptions: MdxOptions, mdxOptions: MdxOptions,
config: AstroConfig config: AstroConfig
): Promise<MdxRollupPluginOptions['remarkPlugins']> { ): Promise<MdxRollupPluginOptions['remarkPlugins']> {
let remarkPlugins: PluggableList = []; let remarkPlugins: PluggableList = [...(config.experimental.assets ? [remarkCollectImages, remarkImageToComponent] : [])];
if (!isPerformanceBenchmark) { if (!isPerformanceBenchmark) {
if (mdxOptions.gfm) { if (mdxOptions.gfm) {

View file

@ -0,0 +1,98 @@
import type { MarkdownVFile } from '@astrojs/markdown-remark';
import { type Image, type Parent } from 'mdast';
import type { MdxJsxFlowElement, MdxjsEsm } from 'mdast-util-mdx';
import { visit } from 'unist-util-visit';
import { jsToTreeNode } from './utils.js';
export function remarkImageToComponent() {
return function (tree: any, file: MarkdownVFile) {
if (!file.data.imagePaths) return;
const importsStatements: MdxjsEsm[] = [];
const importedImages = new Map<string, string>();
visit(tree, 'image', (node: Image, index: number | null, 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 <Image src={importName} alt={node.alt} title={node.title} />
const componentElement: MdxJsxFlowElement = {
name: '__AstroImage__',
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,
});
}
parent!.children.splice(index!, 1, componentElement);
}
});
// Add all the import statements to the top of the file for the images
tree.children.unshift(...importsStatements);
// Add an import statement for the Astro Image component, we rename it to avoid conflicts
tree.children.unshift(jsToTreeNode(`import { Image as __AstroImage__ } from "astro:assets";`));
};
}

View file

@ -0,0 +1,8 @@
import mdx from '@astrojs/mdx';
export default {
integrations: [mdx()],
experimental: {
assets: true
}
}

View file

@ -0,0 +1,9 @@
{
"name": "@test/mdx-page",
"dependencies": {
"@astrojs/mdx": "workspace:*",
"astro": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -0,0 +1,11 @@
Image using a relative path:
![Houston](../assets/houston.webp)
Image using an aliased path:
![Houston](~/assets/houston.webp)
Image with a title:
![Houston](~/assets/houston.webp "Houston title")
Image with spaces in the path:
![Houston](<~/assets/houston in space.webp>)

View file

@ -0,0 +1,40 @@
import { expect } from 'chai';
import { parseHTML } from 'linkedom';
import { loadFixture } from '../../../astro/test/test-utils.js';
describe('MDX Page', () => {
let devServer;
let fixture;
before(async () => {
fixture = await loadFixture({
root: new URL('./fixtures/mdx-images/', import.meta.url),
});
devServer = await fixture.startDevServer();
});
after(async () => {
await devServer.stop();
});
describe('Optimized images in MDX', () => {
it('works', async () => {
const res = await fixture.fetch('/');
expect(res.status).to.equal(200);
const html = await res.text();
const { document } = parseHTML(html);
const imgs = document.getElementsByTagName('img');
expect(imgs.length).to.equal(4);
// Image using a relative path
expect(imgs.item(0).src.startsWith('/_image')).to.be.true;
// Image using an aliased path
expect(imgs.item(1).src.startsWith('/_image')).to.be.true;
// Image with title
expect(imgs.item(2).title).to.equal('Houston title');
// Image with spaces in the path
expect(imgs.item(3).src.startsWith('/_image')).to.be.true;
});
});
});

View file

@ -8,7 +8,7 @@ import type {
import { toRemarkInitializeAstroData } from './frontmatter-injection.js'; import { toRemarkInitializeAstroData } from './frontmatter-injection.js';
import { loadPlugins } from './load-plugins.js'; import { loadPlugins } from './load-plugins.js';
import { rehypeHeadingIds } from './rehype-collect-headings.js'; import { rehypeHeadingIds } from './rehype-collect-headings.js';
import toRemarkCollectImages from './remark-collect-images.js'; import { remarkCollectImages } from './remark-collect-images.js';
import remarkPrism from './remark-prism.js'; import remarkPrism from './remark-prism.js';
import scopedStyles from './remark-scoped-styles.js'; import scopedStyles from './remark-scoped-styles.js';
import remarkShiki from './remark-shiki.js'; import remarkShiki from './remark-shiki.js';
@ -24,6 +24,7 @@ import { VFile } from 'vfile';
import { rehypeImages } from './rehype-images.js'; import { rehypeImages } from './rehype-images.js';
export { rehypeHeadingIds } from './rehype-collect-headings.js'; export { rehypeHeadingIds } from './rehype-collect-headings.js';
export { remarkCollectImages } from './remark-collect-images.js';
export * from './types.js'; export * from './types.js';
export const markdownConfigDefaults: Omit<Required<AstroMarkdownOptions>, 'drafts'> = { export const markdownConfigDefaults: Omit<Required<AstroMarkdownOptions>, 'drafts'> = {
@ -96,7 +97,7 @@ export async function renderMarkdown(
if (opts.experimentalAssets) { if (opts.experimentalAssets) {
// Apply later in case user plugins resolve relative image paths // Apply later in case user plugins resolve relative image paths
parser.use([toRemarkCollectImages()]); parser.use([remarkCollectImages]);
} }
} }

View file

@ -2,9 +2,8 @@ import type { Image } from 'mdast';
import { visit } from 'unist-util-visit'; import { visit } from 'unist-util-visit';
import type { MarkdownVFile } from './types'; import type { MarkdownVFile } from './types';
export default function toRemarkCollectImages() { export function remarkCollectImages() {
return () => return function (tree: any, vfile: MarkdownVFile) {
async function (tree: any, vfile: MarkdownVFile) {
if (typeof vfile?.path !== 'string') return; if (typeof vfile?.path !== 'string') return;
const imagePaths = new Set<string>(); const imagePaths = new Set<string>();

View file

@ -4085,6 +4085,21 @@ importers:
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.2 version: 4.1.2
packages/integrations/mdx/test/fixtures/mdx-images:
dependencies:
'@astrojs/mdx':
specifier: workspace:*
version: link:../../..
astro:
specifier: workspace:*
version: link:../../../../../astro
react:
specifier: ^18.2.0
version: 18.2.0
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
packages/integrations/mdx/test/fixtures/mdx-infinite-loop: packages/integrations/mdx/test/fixtures/mdx-infinite-loop:
dependencies: dependencies:
'@astrojs/mdx': '@astrojs/mdx':