Skip to content

Commit d8305f8

Browse files
fix: preserve HAST properties in image processing (#14902)
Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev>
1 parent 141c676 commit d8305f8

File tree

3 files changed

+58
-5
lines changed

3 files changed

+58
-5
lines changed

.changeset/pink-experts-feel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@astrojs/markdown-remark': patch
3+
---
4+
5+
Prevents HAST-only props from being directly converted into HTML attributes

packages/markdown/remark/src/rehype-images.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@ import type { Properties, Root } from 'hast';
22
import { visit } from 'unist-util-visit';
33
import type { VFile } from 'vfile';
44

5+
/**
6+
* HAST properties to preserve on the node
7+
*/
8+
const HAST_PRESERVED_PROPERTIES = [
9+
// HAST: className -> HTML: class
10+
'className',
11+
12+
// HAST: htmlFor -> HTML: for
13+
'htmlFor',
14+
];
15+
516
export function rehypeImages() {
617
return function (tree: Root, file: VFile) {
718
if (!file.data.astro?.localImagePaths?.length && !file.data.astro?.remoteImagePaths?.length) {
@@ -16,13 +27,13 @@ export function rehypeImages() {
1627
if (typeof node.properties?.src !== 'string') return;
1728

1829
const src = decodeURI(node.properties.src);
19-
let newProperties: Properties;
30+
let imageProperties: Properties;
2031

2132
if (file.data.astro?.localImagePaths?.includes(src)) {
2233
// Override the original `src` with the new, decoded `src` that Astro will better understand.
23-
newProperties = { ...node.properties, src };
34+
imageProperties = { ...node.properties, src };
2435
} else if (file.data.astro?.remoteImagePaths?.includes(src)) {
25-
newProperties = {
36+
imageProperties = {
2637
// By default, markdown images won't have width and height set. However, just in case another user plugin does set these, we should respect them.
2738
inferSize: 'width' in node.properties && 'height' in node.properties ? undefined : true,
2839
...node.properties,
@@ -33,12 +44,21 @@ export function rehypeImages() {
3344
return;
3445
}
3546

47+
// Separate HAST-only properties from image processing properties
48+
const hastProperties: Properties = {};
49+
for (const key of HAST_PRESERVED_PROPERTIES) {
50+
if (key in imageProperties) {
51+
hastProperties[key] = imageProperties[key];
52+
delete imageProperties[key];
53+
}
54+
}
55+
3656
// Initialize or increment occurrence count for this image
3757
const index = imageOccurrenceMap.get(node.properties.src) || 0;
3858
imageOccurrenceMap.set(node.properties.src, index + 1);
3959

4060
// Set a special property on the image so later Astro code knows to process this image.
41-
node.properties = { __ASTRO_IMAGE_: JSON.stringify({ ...newProperties, index }) };
61+
node.properties = { ...hastProperties, __ASTRO_IMAGE_: JSON.stringify({ ...imageProperties, index }) };
4262
});
4363
};
4464
}

packages/markdown/remark/test/remark-collect-images.test.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,28 @@
11
import assert from 'node:assert/strict';
22
import { before, describe, it } from 'node:test';
3+
import { visit } from 'unist-util-visit';
34
import { createMarkdownProcessor } from '../dist/index.js';
45

56
describe('collect images', async () => {
67
let processor;
8+
let processorWithHastProperties;
79

810
before(async () => {
9-
processor = await createMarkdownProcessor({ image: { domains: ['example.com'] } });
11+
processor = await createMarkdownProcessor({ image: { domains: ['example.com'] }, });
12+
processorWithHastProperties = await createMarkdownProcessor({
13+
rehypePlugins: [
14+
() => {
15+
return (tree) => {
16+
visit(tree, 'element', (node) => {
17+
if (node.tagName === 'img') {
18+
node.properties.className = ['image-class'];
19+
node.properties.htmlFor = 'some-id';
20+
}
21+
});
22+
};
23+
},
24+
],
25+
});
1026
});
1127

1228
it('should collect inline image paths', async () => {
@@ -75,4 +91,16 @@ describe('collect images', async () => {
7591
assert.deepEqual(metadata.localImagePaths, ['./img.webp']);
7692
assert.deepEqual(metadata.remoteImagePaths, ['https://example.com/example.jpg']);
7793
});
94+
95+
it('should preserve className as HTML class attribute', async () => {
96+
const markdown = `Hello ![image with class](./img.png)`;
97+
const fileURL = 'file.md';
98+
99+
const { code } = await processorWithHastProperties.render(markdown, { fileURL });
100+
101+
assert.equal(
102+
code,
103+
'<p>Hello <img class="image-class" for="some-id" __ASTRO_IMAGE_="{&#x22;src&#x22;:&#x22;./img.png&#x22;,&#x22;alt&#x22;:&#x22;image with class&#x22;,&#x22;index&#x22;:0}"></p>',
104+
);
105+
});
78106
});

0 commit comments

Comments
 (0)