Skip to content

Commit

Permalink
Allow remark plugins to affect getImage call for .md files (#9566)
Browse files Browse the repository at this point in the history
* pass hProperties to getImage for optimized imgs

* fix to allow multiple images to have hProps added

* update test to reflect new expected result

* add comment back in

Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com>

* add srcset

* works on multiple images

* fix tests, fix images.ts type and remove console logs

* add warning back to images.ts again lol

* update changeset to be user oriented

* Update calm-socks-shake.md

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* pass alt through getImage

* added fixture and test

* update lockfile

* fix lockfile again (had installed an extra package during testing and had sharp33 installed)

* update test to reflect passing alt through getImage

---------

Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com>
Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
  • Loading branch information
3 people committed Jan 17, 2024
1 parent e9a72d9 commit 165cfc1
Show file tree
Hide file tree
Showing 12 changed files with 215 additions and 58 deletions.
6 changes: 6 additions & 0 deletions .changeset/calm-socks-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@astrojs/markdown-remark": minor
"astro": minor
---

Allows remark plugins to pass options specifying how images in `.md` files will be optimized
73 changes: 50 additions & 23 deletions packages/astro/src/vite-plugin-markdown/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,60 @@ export type MarkdownImagePath = { raw: string; resolved: string; safeName: strin

export function getMarkdownCodeForImages(imagePaths: MarkdownImagePath[], html: string) {
return `
import { getImage } from "astro:assets";
${imagePaths
.map((entry) => `import Astro__${entry.safeName} from ${JSON.stringify(entry.raw)};`)
.join('\n')}
import { getImage } from "astro:assets";
${imagePaths
.map((entry) => `import Astro__${entry.safeName} from ${JSON.stringify(entry.raw)};`)
.join('\n')}
const images = async function() {
return {
${imagePaths
.map((entry) => `"${entry.raw}": await getImage({src: Astro__${entry.safeName}})`)
.join(',\n')}
}
}
const images = async function(html) {
const imageSources = {};
${imagePaths
.map((entry) => {
const rawUrl = JSON.stringify(entry.raw);
return `{
const regex = new RegExp('__ASTRO_IMAGE_="([^"]*' + ${rawUrl} + '[^"]*)"', 'g');
let match;
let occurrenceCounter = 0;
while ((match = regex.exec(html)) !== null) {
const matchKey = ${rawUrl} + '_' + occurrenceCounter;
const imageProps = JSON.parse(match[1].replace(/&#x22;/g, '"'));
const { src, ...props } = imageProps;
imageSources[matchKey] = await getImage({src: Astro__${entry.safeName}, ...props});
occurrenceCounter++;
}
}`;
})
.join('\n')}
return imageSources;
};
async function updateImageReferences(html) {
return images().then((images) => {
return html.replaceAll(/__ASTRO_IMAGE_="([^"]+)"/gm, (full, imagePath) =>
spreadAttributes({
src: images[imagePath].src,
...images[imagePath].attributes,
})
);
return images(html).then((imageSources) => {
return html.replaceAll(/__ASTRO_IMAGE_="([^"]+)"/gm, (full, imagePath) => {
const decodedImagePath = JSON.parse(imagePath.replace(/&#x22;/g, '"'));
// Use the 'index' property for each image occurrence
const srcKey = decodedImagePath.src + '_' + decodedImagePath.index;
if (imageSources[srcKey].srcSet && imageSources[srcKey].srcSet.values.length > 0) {
imageSources[srcKey].attributes.srcset = imageSources[srcKey].srcSet.attribute;
}
const { index, ...attributesWithoutIndex } = imageSources[srcKey].attributes;
return spreadAttributes({
src: imageSources[srcKey].src,
...attributesWithoutIndex,
});
});
});
}
}
// NOTE: This causes a top-level await to appear in the user's code, which can break very easily due to a Rollup
// bug and certain adapters not supporting it correctly. See: https://github.com/rollup/rollup/issues/4708
// Tread carefully!
// NOTE: This causes a top-level await to appear in the user's code, which can break very easily due to a Rollup
// bug and certain adapters not supporting it correctly. See: https://github.com/rollup/rollup/issues/4708
// Tread carefully!
const html = await updateImageReferences(${JSON.stringify(html)});
`;
`;
}
60 changes: 60 additions & 0 deletions packages/astro/test/core-image-remark-imgattr.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { Writable } from 'node:stream';

import { Logger } from '../dist/core/logger/core.js';
import { loadFixture } from './test-utils.js';

describe('astro:image', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;

describe('dev', () => {
/** @type {import('./test-utils').DevServer} */
let devServer;
/** @type {Array<{ type: any, level: 'error', message: string; }>} */
let logs = [];

before(async () => {
fixture = await loadFixture({
root: './fixtures/core-image-remark-imgattr/',
});

devServer = await fixture.startDevServer({
logger: new Logger({
level: 'error',
dest: new Writable({
objectMode: true,
write(event, _, callback) {
logs.push(event);
callback();
},
}),
}),
});
});

after(async () => {
await devServer.stop();
});

describe('Test image attributes can be added by remark plugins', () => {
let $;
before(async () => {
let res = await fixture.fetch('/');
let html = await res.text();
$ = cheerio.load(html);
});

it('Image has eager loading meaning getImage passed props it doesnt use through it', async () => {
let $img = $('img');
expect($img.attr('loading')).to.equal('eager');
});

it('Image src contains w=50 meaning getImage correctly used props added through the remark plugin', async () => {
let $img = $('img');
expect(new URL($img.attr('src'), 'http://example.com').searchParams.get('w')).to.equal('50');
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from 'astro/config';
import plugin from "./remarkPlugin"

// https://astro.build/config
export default defineConfig({
markdown: {
remarkPlugins:[plugin]
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "@test/core-image-remark-imgattr",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
},
"scripts": {
"dev": "astro dev"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export default function plugin() {
return transformer;

function transformer(tree) {
function traverse(node) {
if (node.type === "image") {
node.data = node.data || {};
node.data.hProperties = node.data.hProperties || {};
node.data.hProperties.loading = "eager";
node.data.hProperties.width = "50";
}

if (node.children) {
node.children.forEach(traverse);
}
}

traverse(tree);
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
![alt](../assets/penguin2.jpg)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "astro/tsconfigs/base",
"compilerOptions": {
"baseUrl": ".",
}
}
35 changes: 23 additions & 12 deletions packages/markdown/remark/src/rehype-images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,28 @@ import type { MarkdownVFile } from './types.js';

export function rehypeImages() {
return () =>
function (tree: any, file: MarkdownVFile) {
visit(tree, (node) => {
if (node.type !== 'element') return;
if (node.tagName !== 'img') return;
function (tree: any, file: MarkdownVFile) {
const imageOccurrenceMap = new Map();

if (node.properties?.src) {
if (file.data.imagePaths?.has(node.properties.src)) {
node.properties['__ASTRO_IMAGE_'] = node.properties.src;
delete node.properties.src;
}
}
});
};
visit(tree, (node) => {
if (node.type !== 'element') return;
if (node.tagName !== 'img') return;

if (node.properties?.src) {
if (file.data.imagePaths?.has(node.properties.src)) {
const { ...props } = node.properties;

// Initialize or increment occurrence count for this image
const index = imageOccurrenceMap.get(node.properties.src) || 0;
imageOccurrenceMap.set(node.properties.src, index + 1);

node.properties['__ASTRO_IMAGE_'] = JSON.stringify({ ...props, index });

Object.keys(props).forEach((prop) => {
delete node.properties[prop];
});
}
}
});
};
}
46 changes: 23 additions & 23 deletions packages/markdown/remark/test/remark-collect-images.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,32 @@ import { createMarkdownProcessor } from '../dist/index.js';
import chai from 'chai';

describe('collect images', async () => {
const processor = await createMarkdownProcessor();
const processor = await createMarkdownProcessor();

it('should collect inline image paths', async () => {
const {
code,
metadata: { imagePaths },
} = await processor.render(`Hello ![inline image url](./img.png)`, {
fileURL: 'file.md',
});
it('should collect inline image paths', async () => {
const {
code,
metadata: { imagePaths },
} = await processor.render(`Hello ![inline image url](./img.png)`, {
fileURL: 'file.md',
});

chai
.expect(code)
.to.equal('<p>Hello <img alt="inline image url" __ASTRO_IMAGE_="./img.png"></p>');
chai
.expect(code)
.to.equal('<p>Hello <img __ASTRO_IMAGE_="{&#x22;src&#x22;:&#x22;./img.png&#x22;,&#x22;alt&#x22;:&#x22;inline image url&#x22;,&#x22;index&#x22;:0}"></p>');

chai.expect(Array.from(imagePaths)).to.deep.equal(['./img.png']);
});
chai.expect(Array.from(imagePaths)).to.deep.equal(['./img.png']);
});

it('should add image paths from definition', async () => {
const {
code,
metadata: { imagePaths },
} = await processor.render(`Hello ![image ref][img-ref]\n\n[img-ref]: ./img.webp`, {
fileURL: 'file.md',
});
it('should add image paths from definition', async () => {
const {
code,
metadata: { imagePaths },
} = await processor.render(`Hello ![image ref][img-ref]\n\n[img-ref]: ./img.webp`, {
fileURL: 'file.md',
});

chai.expect(code).to.equal('<p>Hello <img alt="image ref" __ASTRO_IMAGE_="./img.webp"></p>');
chai.expect(Array.from(imagePaths)).to.deep.equal(['./img.webp']);
});
chai.expect(code).to.equal('<p>Hello <img __ASTRO_IMAGE_="{&#x22;src&#x22;:&#x22;./img.webp&#x22;,&#x22;alt&#x22;:&#x22;image ref&#x22;,&#x22;index&#x22;:0}"></p>');
chai.expect(Array.from(imagePaths)).to.deep.equal(['./img.webp']);
});
});
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 165cfc1

Please sign in to comment.