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);
}
}
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.