diff --git a/src/runtime/image.ts b/src/runtime/image.ts index a49cfb860..c3de92a27 100644 --- a/src/runtime/image.ts +++ b/src/runtime/image.ts @@ -1,6 +1,17 @@ import { defu } from 'defu' import { hasProtocol, parseURL, joinURL, withLeadingSlash } from 'ufo' -import type { ImageOptions, ImageSizesOptions, CreateImageOptions, ResolvedImage, ImageCTX, $Img, ImageSizes, ImageSizesVariant } from '../types/image' +import type { + ImageOptions, + ImageSizesOptions, + CreateImageOptions, + ResolvedImage, + ImageCTX, + $Img, + ImageSizes, + ImageSizesVariant, + WidthAndHeightFromSize, + HandleVariantHeightOptions +} from '../types/image' import { imageMeta } from './utils/meta' import { checkDensities, parseDensities, parseSize, parseSizes } from './utils' import { prerenderStaticImages } from './utils/prerender' @@ -201,39 +212,86 @@ function getSizes (ctx: ImageCTX, input: string, opts: ImageSizesOptions): Image } } -function getSizesVariant (key: string, size: string, height: number | undefined, hwRatio: number, ctx: ImageCTX): ImageSizesVariant | undefined { - const screenMaxWidth = (ctx.options.screens && ctx.options.screens[key]) || parseInt(key) - const isFluid = size.endsWith('vw') - if (!isFluid && /^\d+$/.test(size)) { - size = size + 'px' +function getWidthAndHeightFromSize (size: string): WidthAndHeightFromSize { + if (size.includes('/')) { + const [widthFromSize, heightFromSize] = size.split('/') + return { widthFromSize, heightFromSize } } - if (!isFluid && !size.endsWith('px')) { + return { widthFromSize: size, heightFromSize: null } +} + +function handleVariantHeight ({ + heightFromSize, + hwRatio, + _cWidth, + height +}: HandleVariantHeightOptions): number | undefined { + if (heightFromSize) { + return parseInt(heightFromSize) + } + if (hwRatio) { + return Math.round(_cWidth * hwRatio) + } + return height +} + +function getSizesVariant ( + key: string, + size: string, + height: number | undefined, + hwRatio: number, + ctx: ImageCTX +): ImageSizesVariant | undefined { + const screenMaxWidth = + (ctx.options.screens && ctx.options.screens[key]) || parseInt(key) + + // eslint-disable-next-line prefer-const + let { widthFromSize, heightFromSize } = getWidthAndHeightFromSize(size) + const isFluid = widthFromSize.endsWith('vw') + if (!isFluid && /^\d+$/.test(widthFromSize)) { + widthFromSize = widthFromSize + 'px' + } + if (!isFluid && !widthFromSize.endsWith('px')) { return undefined } - let _cWidth = parseInt(size) + let _cWidth = parseInt(widthFromSize) if (!screenMaxWidth || !_cWidth) { return undefined } if (isFluid) { _cWidth = Math.round((_cWidth / 100) * screenMaxWidth) } - const _cHeight = hwRatio ? Math.round(_cWidth * hwRatio) : height + + const _cHeight = handleVariantHeight({ + heightFromSize, + hwRatio, + _cWidth, + height + }) return { - size, + size: widthFromSize, screenMaxWidth, _cWidth, _cHeight } } -function getVariantSrc (ctx: ImageCTX, input: string, opts: ImageSizesOptions, variant: ImageSizesVariant, density: number) { - return ctx.$img!(input, +function getVariantSrc ( + ctx: ImageCTX, + input: string, + opts: ImageSizesOptions, + variant: ImageSizesVariant, + density: number +) { + return ctx.$img!( + input, { ...opts.modifiers, width: variant._cWidth ? variant._cWidth * density : undefined, height: variant._cHeight ? variant._cHeight * density : undefined }, - opts) + opts + ) } function finaliseSizeVariants (sizeVariants: any[]) { diff --git a/src/runtime/utils/index.ts b/src/runtime/utils/index.ts index 3f4532fac..afbd2a1a0 100644 --- a/src/runtime/utils/index.ts +++ b/src/runtime/utils/index.ts @@ -108,24 +108,33 @@ export function parseDensities (input: string | undefined = ''): number[] { export function checkDensities (densities: number[]) { if (densities.length === 0) { - throw new Error('`densities` must not be empty, configure to `1` to render regular size only (DPR 1.0)') + throw new Error( + '`densities` must not be empty, configure to `1` to render regular size only (DPR 1.0)' + ) } if (process.dev && Array.from(densities).some(d => d > 2)) { // eslint-disable-next-line no-console - console.warn('[nuxt] [image] Density values above `2` are not recommended. See https://observablehq.com/@eeeps/visual-acuity-and-device-pixel-ratio.') + console.warn( + '[nuxt] [image] Density values above `2` are not recommended. See https://observablehq.com/@eeeps/visual-acuity-and-device-pixel-ratio.' + ) } } -export function parseSizes (input: Record | string): Record { +export function parseSizes ( + input: Record | string +): Record { const sizes: Record = {} // string => object if (typeof input === 'string') { for (const entry of input.split(/[\s,]+/).filter(e => e)) { - const s = entry.split(':') - if (s.length !== 2) { - sizes['1px'] = s[0].trim() + const sizeParts = entry.split(':') + if (sizeParts.length !== 2) { + const [size] = sizeParts + const DEFAULT_BREAKPOINT = '1px' + sizes[DEFAULT_BREAKPOINT] = size.trim() } else { - sizes[s[0].trim()] = s[1].trim() + const [breakpoint, size] = sizeParts + sizes[breakpoint.trim()] = size.trim() } } } else { diff --git a/src/types/image.ts b/src/types/image.ts index fd8ed6805..2e493c61d 100644 --- a/src/types/image.ts +++ b/src/types/image.ts @@ -116,3 +116,15 @@ export interface ImageSizesVariant { _cWidth: number _cHeight?: number | undefined } + +export type WidthAndHeightFromSize = { + widthFromSize: string; + heightFromSize: string | null; +}; + +export type HandleVariantHeightOptions = { + heightFromSize:WidthAndHeightFromSize['heightFromSize']; + hwRatio:number; + _cWidth:number; + height?:number; +} diff --git a/test/unit/image.test.ts b/test/unit/image.test.ts index d7036e122..0658ca16a 100644 --- a/test/unit/image.test.ts +++ b/test/unit/image.test.ts @@ -156,6 +156,15 @@ describe('Renders simple image', () => { const domNonce = img.element.getAttribute('nonce') expect(domNonce).toBe('stub-nonce') }) + + it('works with responsive height', () => { + const img = mountImage({ + src: '/image.png', + sizes: '100px/20px sm:100px/10px md:10px lg:200px/50px', + densities: 'x1 x2' + }) + expect(img.html()).toMatchInlineSnapshot('""') + }) }) const getImageLoad = (cb = () => {}) => {