mirror of
https://github.com/withastro/astro.git
synced 2025-03-31 23:31:30 -05:00
Allow remark plugins to affect getImage call for .md files (#9566)
* 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>
This commit is contained in:
parent
e9a72d9a91
commit
165cfc154b
12 changed files with 215 additions and 58 deletions
6
.changeset/calm-socks-shake.md
Normal file
6
.changeset/calm-socks-shake.md
Normal file
|
@ -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
|
|
@ -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
packages/astro/test/core-image-remark-imgattr.test.js
Normal file
60
packages/astro/test/core-image-remark-imgattr.test.js
Normal file
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
9
packages/astro/test/fixtures/core-image-remark-imgattr/astro.config.mjs
vendored
Normal file
9
packages/astro/test/fixtures/core-image-remark-imgattr/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import plugin from "./remarkPlugin"
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
markdown: {
|
||||
remarkPlugins:[plugin]
|
||||
}
|
||||
});
|
11
packages/astro/test/fixtures/core-image-remark-imgattr/package.json
vendored
Normal file
11
packages/astro/test/fixtures/core-image-remark-imgattr/package.json
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "@test/core-image-remark-imgattr",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "astro dev"
|
||||
}
|
||||
}
|
20
packages/astro/test/fixtures/core-image-remark-imgattr/remarkPlugin.js
vendored
Normal file
20
packages/astro/test/fixtures/core-image-remark-imgattr/remarkPlugin.js
vendored
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
BIN
packages/astro/test/fixtures/core-image-remark-imgattr/src/assets/penguin2.jpg
vendored
Normal file
BIN
packages/astro/test/fixtures/core-image-remark-imgattr/src/assets/penguin2.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
1
packages/astro/test/fixtures/core-image-remark-imgattr/src/pages/index.md
vendored
Normal file
1
packages/astro/test/fixtures/core-image-remark-imgattr/src/pages/index.md
vendored
Normal file
|
@ -0,0 +1 @@
|
|||

|
6
packages/astro/test/fixtures/core-image-remark-imgattr/tsconfig.json
vendored
Normal file
6
packages/astro/test/fixtures/core-image-remark-imgattr/tsconfig.json
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/base",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 `, {
|
||||
fileURL: 'file.md',
|
||||
});
|
||||
it('should collect inline image paths', async () => {
|
||||
const {
|
||||
code,
|
||||
metadata: { imagePaths },
|
||||
} = await processor.render(`Hello `, {
|
||||
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_="{"src":"./img.png","alt":"inline image url","index":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_="{"src":"./img.webp","alt":"image ref","index":0}"></p>');
|
||||
chai.expect(Array.from(imagePaths)).to.deep.equal(['./img.webp']);
|
||||
});
|
||||
});
|
||||
|
|
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
|
@ -2523,6 +2523,12 @@ importers:
|
|||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
|
||||
packages/astro/test/fixtures/core-image-remark-imgattr:
|
||||
dependencies:
|
||||
astro:
|
||||
specifier: workspace:*
|
||||
version: link:../../..
|
||||
|
||||
packages/astro/test/fixtures/core-image-ssg:
|
||||
dependencies:
|
||||
astro:
|
||||
|
|
Loading…
Add table
Reference in a new issue