Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow remark plugins to affect getImage call for .md files #9566

Merged
merged 18 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(/"/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(/"/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.

Loading