diff --git a/.changeset/empty-badgers-happen.md b/.changeset/empty-badgers-happen.md new file mode 100644 index 000000000..75eaad716 --- /dev/null +++ b/.changeset/empty-badgers-happen.md @@ -0,0 +1,5 @@ +--- +"gitbook-v2": patch +--- + +Only resize images with supported extensions. diff --git a/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.test.ts b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.test.ts new file mode 100644 index 000000000..679f2c5ce --- /dev/null +++ b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.test.ts @@ -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 + ); + }); +}); diff --git a/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts index 61e3ebbc2..486ea7a69 100644 --- a/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts +++ b/packages/gitbook-v2/src/lib/images/checkIsSizableImageURL.ts @@ -1,9 +1,16 @@ +import { getExtension } from '@/lib/paths'; + export enum SizableImageAction { Resize = 'resize', Skip = 'skip', 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. * 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')) { 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; } diff --git a/packages/gitbook/src/components/DocumentView/Embed.tsx b/packages/gitbook/src/components/DocumentView/Embed.tsx index 796cdaeee..17849c195 100644 --- a/packages/gitbook/src/components/DocumentView/Embed.tsx +++ b/packages/gitbook/src/components/DocumentView/Embed.tsx @@ -6,6 +6,7 @@ import { Card } from '@/components/primitives'; import { tcls } from '@/lib/tailwind'; import { getDataOrNull } from '@v2/lib/data'; +import { Image } from '../utils'; import type { BlockProps } from './Block'; import { Caption } from './Caption'; import { IntegrationBlock } from './Integration'; @@ -52,7 +53,14 @@ export async function Embed(props: BlockProps) { + Logo ) : null } href={block.data.url} diff --git a/packages/gitbook/src/lib/paths.test.ts b/packages/gitbook/src/lib/paths.test.ts new file mode 100644 index 000000000..1d418f498 --- /dev/null +++ b/packages/gitbook/src/lib/paths.test.ts @@ -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'); + }); +}); diff --git a/packages/gitbook/src/lib/paths.ts b/packages/gitbook/src/lib/paths.ts index eff69a569..ebdf35cd3 100644 --- a/packages/gitbook/src/lib/paths.ts +++ b/packages/gitbook/src/lib/paths.ts @@ -57,3 +57,15 @@ export function withTrailingSlash(pathname: string): string { 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 ''; +}