0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-03-10 23:01:26 -05:00

feat(markdoc): Add support for using a custom component for images (#9958)

* feat(markdoc): Add support for using a custom component for images

* chore: changeset

* test: add test

* Update .changeset/shaggy-spies-sit.md

Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev>

---------

Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev>
This commit is contained in:
Erika 2024-02-08 07:54:40 +01:00 committed by GitHub
parent 8fb67c81bb
commit 14ce8a6ebf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 117 additions and 21 deletions

View file

@ -0,0 +1,41 @@
---
"@astrojs/markdoc": minor
---
Adds support for using a custom tag (component) for optimized images
Starting from this version, when a tag called `image` is used, its `src` attribute will automatically be resolved if it's a local image. Astro will pass the result `ImageMetadata` object to the underlying component as the `src` prop. For non-local images (i.e. images using URLs or absolute paths), Astro will continue to pass the `src` as a string.
```ts
// markdoc.config.mjs
import { component, defineMarkdocConfig, nodes } from '@astrojs/markdoc/config';
export default defineMarkdocConfig({
tags: {
image: {
attributes: nodes.image.attributes,
render: component('./src/components/MarkdocImage.astro'),
},
},
});
```
```astro
---
// src/components/MarkdocImage.astro
import { Image } from "astro:assets";
interface Props {
src: ImageMetadata | string;
alt: string;
width: number;
height: number;
}
const { src, alt, width, height } = Astro.props;
---
<Image {src} {alt} {width} {height} />
```
```mdoc
{% image src="./astro-logo.png" alt="Astro Logo" width="100" height="100" %}
``````

View file

@ -179,28 +179,35 @@ async function emitOptimizedImages(
}
) {
for (const node of nodeChildren) {
if (
node.type === 'image' &&
typeof node.attributes.src === 'string' &&
shouldOptimizeImage(node.attributes.src)
) {
// Attempt to resolve source with Vite.
// This handles relative paths and configured aliases
const resolved = await ctx.pluginContext.resolve(node.attributes.src, ctx.filePath);
let isComponent = node.type === 'tag' && node.tag === 'image';
// Support either a ![]() or {% image %} syntax, and handle the `src` attribute accordingly.
if ((node.type === 'image' || isComponent) && typeof node.attributes.src === 'string') {
let attributeName = isComponent ? 'src' : '__optimizedSrc';
if (resolved?.id && fs.existsSync(new URL(prependForwardSlash(resolved.id), 'file://'))) {
const src = await emitESMImage(
resolved.id,
ctx.pluginContext.meta.watchMode,
ctx.pluginContext.emitFile
);
node.attributes.__optimizedSrc = src;
} else {
throw new MarkdocError({
message: `Could not resolve image ${JSON.stringify(
node.attributes.src
)} from ${JSON.stringify(ctx.filePath)}. Does the file exist?`,
});
// If the image isn't an URL or a link to public, try to resolve it.
if (shouldOptimizeImage(node.attributes.src)) {
// Attempt to resolve source with Vite.
// This handles relative paths and configured aliases
const resolved = await ctx.pluginContext.resolve(node.attributes.src, ctx.filePath);
if (resolved?.id && fs.existsSync(new URL(prependForwardSlash(resolved.id), 'file://'))) {
const src = await emitESMImage(
resolved.id,
ctx.pluginContext.meta.watchMode,
ctx.pluginContext.emitFile
);
node.attributes[attributeName] = src;
} else {
throw new MarkdocError({
message: `Could not resolve image ${JSON.stringify(
node.attributes.src
)} from ${JSON.stringify(ctx.filePath)}. Does the file exist?`,
});
}
} else if (isComponent) {
// If the user is using the {% image %} tag, always pass the `src` attribute as `__optimizedSrc`, even if it's an external URL or absolute path.
// That way, the component can decide whether to optimize it or not.
node.attributes[attributeName] = node.attributes.src;
}
}
await emitOptimizedImages(node.children, ctx);

View file

@ -0,0 +1,10 @@
import { component, defineMarkdocConfig, nodes } from '@astrojs/markdoc/config';
export default defineMarkdocConfig({
tags: {
image: {
attributes: nodes.image.attributes,
render: component('./src/components/Image.astro'),
},
},
});

View file

@ -0,0 +1,22 @@
---
// src/components/MyImage.astro
import type { ImageMetadata } from 'astro';
import { Image } from 'astro:assets';
type Props = {
src: string | ImageMetadata;
alt: string;
};
const { src, alt } = Astro.props;
---
{
typeof src === 'string' ? (
<img class="custom-styles" src={src} alt={alt} />
) : (
<Image class="custom-styles" {src} {alt} />
)
}
<style>
.custom-styles {
border: 1px solid red;
}
</style>

View file

@ -5,3 +5,5 @@
![Oar](../../assets/relative/oar.jpg) {% #relative %}
![Gray cityscape arial view](~/assets/alias/cityscape.jpg) {% #alias %}
{% image src="../../assets/relative/oar.jpg" alt="oar" /%} {% #component %}

View file

@ -51,6 +51,13 @@ describe('Markdoc - Image assets', () => {
/\/_image\?href=.*%2Fsrc%2Fassets%2Falias%2Fcityscape.jpg%3ForigWidth%3D420%26origHeight%3D280%26origFormat%3Djpg&f=webp/
);
});
it('passes images inside image tags to configured image component', async () => {
const res = await baseFixture.fetch('/');
const html = await res.text();
const { document } = parseHTML(html);
assert.equal(document.querySelector('#component > img')?.className, 'custom-styles');
});
});
describe('build', () => {
@ -75,5 +82,12 @@ describe('Markdoc - Image assets', () => {
const { document } = parseHTML(html);
assert.match(document.querySelector('#alias > img')?.src, /^\/_astro\/cityscape.*\.webp$/);
});
it('passes images inside image tags to configured image component', async () => {
const html = await baseFixture.readFile('/index.html');
const { document } = parseHTML(html);
assert.equal(document.querySelector('#component > img')?.className, 'custom-styles');
assert.match(document.querySelector('#component > img')?.src, /^\/_astro\/oar.*\.webp$/);
});
});
});