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:
parent
8fb67c81bb
commit
14ce8a6ebf
6 changed files with 117 additions and 21 deletions
41
.changeset/shaggy-spies-sit.md
Normal file
41
.changeset/shaggy-spies-sit.md
Normal 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" %}
|
||||
``````
|
|
@ -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);
|
||||
|
|
10
packages/integrations/markdoc/test/fixtures/image-assets/markdoc.config.mjs
vendored
Normal file
10
packages/integrations/markdoc/test/fixtures/image-assets/markdoc.config.mjs
vendored
Normal 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'),
|
||||
},
|
||||
},
|
||||
});
|
22
packages/integrations/markdoc/test/fixtures/image-assets/src/components/Image.astro
vendored
Normal file
22
packages/integrations/markdoc/test/fixtures/image-assets/src/components/Image.astro
vendored
Normal 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>
|
|
@ -5,3 +5,5 @@
|
|||
 {% #relative %}
|
||||
|
||||
 {% #alias %}
|
||||
|
||||
{% image src="../../assets/relative/oar.jpg" alt="oar" /%} {% #component %}
|
||||
|
|
|
@ -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$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue