mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-22 18:43:29 +00:00
Only resize images with supported extensions (#3229)
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"gitbook-v2": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Only resize images with supported extensions.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
import { SizableImageAction, checkIsSizableImageURL } from './checkIsSizableImageURL';
|
||||||
|
|
||||||
|
describe('checkIsSizableImageURL', () => {
|
||||||
|
it('should return Skip for non-parsable URLs', () => {
|
||||||
|
expect(checkIsSizableImageURL('not a url')).toBe(SizableImageAction.Skip);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Skip for non-http(s) URLs', () => {
|
||||||
|
expect(checkIsSizableImageURL('data:image/png;base64,abc')).toBe(SizableImageAction.Skip);
|
||||||
|
expect(checkIsSizableImageURL('file:///path/to/image.jpg')).toBe(SizableImageAction.Skip);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Skip for localhost URLs', () => {
|
||||||
|
expect(checkIsSizableImageURL('http://localhost:3000/image.jpg')).toBe(
|
||||||
|
SizableImageAction.Skip
|
||||||
|
);
|
||||||
|
expect(checkIsSizableImageURL('https://localhost/image.png')).toBe(SizableImageAction.Skip);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Skip for GitBook image URLs', () => {
|
||||||
|
expect(checkIsSizableImageURL('https://example.com/~gitbook/image/test.jpg')).toBe(
|
||||||
|
SizableImageAction.Skip
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Resize for supported image extensions', () => {
|
||||||
|
expect(checkIsSizableImageURL('https://example.com/image.jpg')).toBe(
|
||||||
|
SizableImageAction.Resize
|
||||||
|
);
|
||||||
|
expect(checkIsSizableImageURL('https://example.com/image.jpeg')).toBe(
|
||||||
|
SizableImageAction.Resize
|
||||||
|
);
|
||||||
|
expect(checkIsSizableImageURL('https://example.com/image.png')).toBe(
|
||||||
|
SizableImageAction.Resize
|
||||||
|
);
|
||||||
|
expect(checkIsSizableImageURL('https://example.com/image.gif')).toBe(
|
||||||
|
SizableImageAction.Resize
|
||||||
|
);
|
||||||
|
expect(checkIsSizableImageURL('https://example.com/image.webp')).toBe(
|
||||||
|
SizableImageAction.Resize
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Resize for URLs without extensions', () => {
|
||||||
|
expect(checkIsSizableImageURL('https://example.com/image')).toBe(SizableImageAction.Resize);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Passthrough for unsupported image extensions', () => {
|
||||||
|
expect(checkIsSizableImageURL('https://example.com/image.svg')).toBe(
|
||||||
|
SizableImageAction.Passthrough
|
||||||
|
);
|
||||||
|
expect(checkIsSizableImageURL('https://example.com/image.bmp')).toBe(
|
||||||
|
SizableImageAction.Passthrough
|
||||||
|
);
|
||||||
|
expect(checkIsSizableImageURL('https://example.com/image.tiff')).toBe(
|
||||||
|
SizableImageAction.Passthrough
|
||||||
|
);
|
||||||
|
expect(checkIsSizableImageURL('https://example.com/image.ico')).toBe(
|
||||||
|
SizableImageAction.Passthrough
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle URLs with query parameters correctly', () => {
|
||||||
|
expect(checkIsSizableImageURL('https://example.com/image.jpg?width=100')).toBe(
|
||||||
|
SizableImageAction.Resize
|
||||||
|
);
|
||||||
|
expect(checkIsSizableImageURL('https://example.com/image.svg?height=200')).toBe(
|
||||||
|
SizableImageAction.Passthrough
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be case-insensitive for extensions', () => {
|
||||||
|
expect(checkIsSizableImageURL('https://example.com/image.JPG')).toBe(
|
||||||
|
SizableImageAction.Resize
|
||||||
|
);
|
||||||
|
expect(checkIsSizableImageURL('https://example.com/image.PNG')).toBe(
|
||||||
|
SizableImageAction.Resize
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,9 +1,16 @@
|
|||||||
|
import { getExtension } from '@/lib/paths';
|
||||||
|
|
||||||
export enum SizableImageAction {
|
export enum SizableImageAction {
|
||||||
Resize = 'resize',
|
Resize = 'resize',
|
||||||
Skip = 'skip',
|
Skip = 'skip',
|
||||||
Passthrough = 'passthrough',
|
Passthrough = 'passthrough',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developers.cloudflare.com/images/transform-images/#supported-input-formats
|
||||||
|
*/
|
||||||
|
const SUPPORTED_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if an image URL is resizable.
|
* Check if an image URL is resizable.
|
||||||
* Skip it for non-http(s) URLs (data, etc).
|
* Skip it for non-http(s) URLs (data, etc).
|
||||||
@@ -25,9 +32,12 @@ export function checkIsSizableImageURL(input: string): SizableImageAction {
|
|||||||
if (parsed.pathname.includes('/~gitbook/image')) {
|
if (parsed.pathname.includes('/~gitbook/image')) {
|
||||||
return SizableImageAction.Skip;
|
return SizableImageAction.Skip;
|
||||||
}
|
}
|
||||||
if (parsed.pathname.endsWith('.svg') || parsed.pathname.endsWith('.avif')) {
|
|
||||||
return SizableImageAction.Passthrough;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const extension = getExtension(parsed.pathname).toLowerCase();
|
||||||
|
if (!extension || SUPPORTED_IMAGE_EXTENSIONS.includes(extension)) {
|
||||||
|
// If no extension, we consider it resizable.
|
||||||
return SizableImageAction.Resize;
|
return SizableImageAction.Resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return SizableImageAction.Passthrough;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Card } from '@/components/primitives';
|
|||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
import { getDataOrNull } from '@v2/lib/data';
|
import { getDataOrNull } from '@v2/lib/data';
|
||||||
|
import { Image } from '../utils';
|
||||||
import type { BlockProps } from './Block';
|
import type { BlockProps } from './Block';
|
||||||
import { Caption } from './Caption';
|
import { Caption } from './Caption';
|
||||||
import { IntegrationBlock } from './Integration';
|
import { IntegrationBlock } from './Integration';
|
||||||
@@ -52,7 +53,14 @@ export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
|
|||||||
<Card
|
<Card
|
||||||
leadingIcon={
|
leadingIcon={
|
||||||
embed.icon ? (
|
embed.icon ? (
|
||||||
<img src={embed.icon} className={tcls('w-5', 'h-5')} alt="Logo" />
|
<Image
|
||||||
|
src={embed.icon}
|
||||||
|
className={tcls('w-5', 'h-5')}
|
||||||
|
alt="Logo"
|
||||||
|
sources={{ light: { src: embed.icon } }}
|
||||||
|
sizes={[{ width: 20 }]}
|
||||||
|
resize={context.contentContext.imageResizer}
|
||||||
|
/>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
href={block.data.url}
|
href={block.data.url}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
import { getExtension } from './paths';
|
||||||
|
|
||||||
|
describe('getExtension', () => {
|
||||||
|
it('should return the extension of a path', () => {
|
||||||
|
expect(getExtension('test.txt')).toBe('.txt');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty string if there is no extension', () => {
|
||||||
|
expect(getExtension('test/path/to/file')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the extension of a path with multiple dots', () => {
|
||||||
|
expect(getExtension('test.with.multiple.dots.txt')).toBe('.txt');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -57,3 +57,15 @@ export function withTrailingSlash(pathname: string): string {
|
|||||||
|
|
||||||
return pathname;
|
return pathname;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the extension of a path.
|
||||||
|
*/
|
||||||
|
export function getExtension(path: string): string {
|
||||||
|
const re = /\.[0-9a-z]+$/i;
|
||||||
|
const match = path.match(re);
|
||||||
|
if (match) {
|
||||||
|
return match[0];
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user