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 type { VFile } from 'vfile';
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: VFile) {
if (!file.data.astro?.localImagePaths?.length && !file.data.astro?.remoteImagePaths?.length)
return;
const importsStatements: MdxjsEsm[] = [];
const importedImages = new Map();
visit(tree, 'element', (node, index, parent) => {
if (node.tagName !== 'img' || !node.properties.src) return;
const src = decodeURI(String(node.properties.src));
const isLocalImage = file.data.astro?.localImagePaths?.includes(src);
const isRemoteImage = file.data.astro?.remoteImagePaths?.includes(src);
let element: MdxJsxFlowElementHast;
if (isLocalImage) {
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: [
{
attributes: [],
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
element = {
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: [],
};
} else if (isRemoteImage) {
// Build a component that's equivalent to
element = {
name: ASTRO_IMAGE_ELEMENT,
type: 'mdxJsxFlowElement',
attributes: [
...getImageComponentAttributes(node.properties),
{
name: 'src',
type: 'mdxJsxAttribute',
value: src,
},
],
children: [],
};
} else {
return;
}
parent!.children.splice(index!, 1, element);
});
// 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`));
};
}