Skip to content

Commit 728618c

Browse files
committed
Merge branch 'main' into claire/rnd-6959-add-support-for-tabs-links-in-gbo
* main: Only resize images with supported extensions (#3229)
2 parents 7e7ba9b + 778624a commit 728618c

File tree

6 files changed

+136
-4
lines changed

6 files changed

+136
-4
lines changed

.changeset/empty-badgers-happen.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"gitbook-v2": patch
3+
---
4+
5+
Only resize images with supported extensions.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { describe, expect, it } from 'bun:test';
2+
import { SizableImageAction, checkIsSizableImageURL } from './checkIsSizableImageURL';
3+
4+
describe('checkIsSizableImageURL', () => {
5+
it('should return Skip for non-parsable URLs', () => {
6+
expect(checkIsSizableImageURL('not a url')).toBe(SizableImageAction.Skip);
7+
});
8+
9+
it('should return Skip for non-http(s) URLs', () => {
10+
expect(checkIsSizableImageURL('data:image/png;base64,abc')).toBe(SizableImageAction.Skip);
11+
expect(checkIsSizableImageURL('file:///path/to/image.jpg')).toBe(SizableImageAction.Skip);
12+
});
13+
14+
it('should return Skip for localhost URLs', () => {
15+
expect(checkIsSizableImageURL('http://localhost:3000/image.jpg')).toBe(
16+
SizableImageAction.Skip
17+
);
18+
expect(checkIsSizableImageURL('https://localhost/image.png')).toBe(SizableImageAction.Skip);
19+
});
20+
21+
it('should return Skip for GitBook image URLs', () => {
22+
expect(checkIsSizableImageURL('https://example.com/~gitbook/image/test.jpg')).toBe(
23+
SizableImageAction.Skip
24+
);
25+
});
26+
27+
it('should return Resize for supported image extensions', () => {
28+
expect(checkIsSizableImageURL('https://example.com/image.jpg')).toBe(
29+
SizableImageAction.Resize
30+
);
31+
expect(checkIsSizableImageURL('https://example.com/image.jpeg')).toBe(
32+
SizableImageAction.Resize
33+
);
34+
expect(checkIsSizableImageURL('https://example.com/image.png')).toBe(
35+
SizableImageAction.Resize
36+
);
37+
expect(checkIsSizableImageURL('https://example.com/image.gif')).toBe(
38+
SizableImageAction.Resize
39+
);
40+
expect(checkIsSizableImageURL('https://example.com/image.webp')).toBe(
41+
SizableImageAction.Resize
42+
);
43+
});
44+
45+
it('should return Resize for URLs without extensions', () => {
46+
expect(checkIsSizableImageURL('https://example.com/image')).toBe(SizableImageAction.Resize);
47+
});
48+
49+
it('should return Passthrough for unsupported image extensions', () => {
50+
expect(checkIsSizableImageURL('https://example.com/image.svg')).toBe(
51+
SizableImageAction.Passthrough
52+
);
53+
expect(checkIsSizableImageURL('https://example.com/image.bmp')).toBe(
54+
SizableImageAction.Passthrough
55+
);
56+
expect(checkIsSizableImageURL('https://example.com/image.tiff')).toBe(
57+
SizableImageAction.Passthrough
58+
);
59+
expect(checkIsSizableImageURL('https://example.com/image.ico')).toBe(
60+
SizableImageAction.Passthrough
61+
);
62+
});
63+
64+
it('should handle URLs with query parameters correctly', () => {
65+
expect(checkIsSizableImageURL('https://example.com/image.jpg?width=100')).toBe(
66+
SizableImageAction.Resize
67+
);
68+
expect(checkIsSizableImageURL('https://example.com/image.svg?height=200')).toBe(
69+
SizableImageAction.Passthrough
70+
);
71+
});
72+
73+
it('should be case-insensitive for extensions', () => {
74+
expect(checkIsSizableImageURL('https://example.com/image.JPG')).toBe(
75+
SizableImageAction.Resize
76+
);
77+
expect(checkIsSizableImageURL('https://example.com/image.PNG')).toBe(
78+
SizableImageAction.Resize
79+
);
80+
});
81+
});

packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1+
import { getExtension } from '@/lib/paths';
2+
13
export enum SizableImageAction {
24
Resize = 'resize',
35
Skip = 'skip',
46
Passthrough = 'passthrough',
57
}
68

9+
/**
10+
* https://developers.cloudflare.com/images/transform-images/#supported-input-formats
11+
*/
12+
const SUPPORTED_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
13+
714
/**
815
* Check if an image URL is resizable.
916
* Skip it for non-http(s) URLs (data, etc).
@@ -25,9 +32,12 @@ export function checkIsSizableImageURL(input: string): SizableImageAction {
2532
if (parsed.pathname.includes('/~gitbook/image')) {
2633
return SizableImageAction.Skip;
2734
}
28-
if (parsed.pathname.endsWith('.svg') || parsed.pathname.endsWith('.avif')) {
29-
return SizableImageAction.Passthrough;
35+
36+
const extension = getExtension(parsed.pathname).toLowerCase();
37+
if (!extension || SUPPORTED_IMAGE_EXTENSIONS.includes(extension)) {
38+
// If no extension, we consider it resizable.
39+
return SizableImageAction.Resize;
3040
}
3141

32-
return SizableImageAction.Resize;
42+
return SizableImageAction.Passthrough;
3343
}

packages/gitbook/src/components/DocumentView/Embed.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Card } from '@/components/primitives';
66
import { tcls } from '@/lib/tailwind';
77

88
import { getDataOrNull } from '@v2/lib/data';
9+
import { Image } from '../utils';
910
import type { BlockProps } from './Block';
1011
import { Caption } from './Caption';
1112
import { IntegrationBlock } from './Integration';
@@ -52,7 +53,14 @@ export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
5253
<Card
5354
leadingIcon={
5455
embed.icon ? (
55-
<img src={embed.icon} className={tcls('w-5', 'h-5')} alt="Logo" />
56+
<Image
57+
src={embed.icon}
58+
className={tcls('w-5', 'h-5')}
59+
alt="Logo"
60+
sources={{ light: { src: embed.icon } }}
61+
sizes={[{ width: 20 }]}
62+
resize={context.contentContext.imageResizer}
63+
/>
5664
) : null
5765
}
5866
href={block.data.url}
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { describe, expect, it } from 'bun:test';
2+
import { getExtension } from './paths';
3+
4+
describe('getExtension', () => {
5+
it('should return the extension of a path', () => {
6+
expect(getExtension('test.txt')).toBe('.txt');
7+
});
8+
9+
it('should return an empty string if there is no extension', () => {
10+
expect(getExtension('test/path/to/file')).toBe('');
11+
});
12+
13+
it('should return the extension of a path with multiple dots', () => {
14+
expect(getExtension('test.with.multiple.dots.txt')).toBe('.txt');
15+
});
16+
});

packages/gitbook/src/lib/paths.ts

+12
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,15 @@ export function withTrailingSlash(pathname: string): string {
5757

5858
return pathname;
5959
}
60+
61+
/**
62+
* Get the extension of a path.
63+
*/
64+
export function getExtension(path: string): string {
65+
const re = /\.[0-9a-z]+$/i;
66+
const match = path.match(re);
67+
if (match) {
68+
return match[0];
69+
}
70+
return '';
71+
}

0 commit comments

Comments
 (0)