@ecopages/image-processor 0.2.0-alpha.5 → 0.2.0-alpha.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,427 +0,0 @@
1
- /**
2
- * ImageRenderer
3
- * @module
4
- */
5
-
6
- import { DEFAULT_LAYOUT } from './constants';
7
- import { ImageUtils } from './image-utils';
8
- import type { ImageSpecifications, ImageVariant } from './types';
9
-
10
- /**
11
- * Image layout options
12
- */
13
- export type ImageLayout = 'fixed' | 'constrained' | 'full-width';
14
-
15
- /**
16
- * ImageProps
17
- * This interface represents the properties that can be passed to the image element
18
- */
19
- export type EcoImageProps = ImageSpecifications & {
20
- /**
21
- * width of the image as defined in the component
22
- */
23
- width?: number;
24
- /**
25
- * height of the image as defined in the component
26
- */
27
- height?: number;
28
- /**
29
- * The alt text for the image
30
- */
31
- aspectRatio?: string;
32
- /**
33
- * If true, the image will be loaded eagerly
34
- */
35
- priority?: boolean;
36
- /**
37
- * The layout of the image
38
- * @default "constrained"
39
- */
40
- layout?: ImageLayout;
41
- /**
42
- * If true, the image will not have any styles applied to it
43
- */
44
- unstyled?: boolean;
45
- /**
46
- * Specifies a fixed image variant from the configuration.
47
- * This should match one of the variant labels defined in your image optimization config.
48
- * When set, the image will use this specific variant instead of responsive sizes.
49
- */
50
- staticVariant?: string;
51
- /**
52
- * Optional additional attributes to be added to the image element
53
- */
54
- [additionalAttributes: string]: any;
55
- };
56
-
57
- /**
58
- * CollectedAttributes
59
- * This interface represents the attributes that are collected by the image renderer
60
- * These attributes are used to generate the attributes for the image element
61
- */
62
- export interface CollectedAttributes {
63
- width?: number;
64
- height?: number;
65
- loading: HTMLImageElement['loading'];
66
- fetchpriority: HTMLImageElement['fetchPriority'];
67
- decoding: HTMLImageElement['decoding'];
68
- src: string;
69
- srcset?: string;
70
- sizes?: string;
71
- styles: [string, string][];
72
- }
73
-
74
- /**
75
- * GenerateAttributesResult
76
- * This interface represents the result of the generateAttributes method
77
- */
78
- export interface GenerateAttributesResult {
79
- fetchpriority: HTMLImageElement['fetchPriority'];
80
- loading: HTMLImageElement['loading'];
81
- decoding: HTMLImageElement['decoding'];
82
- src: string;
83
- srcset?: string;
84
- sizes?: string;
85
- width?: number;
86
- height?: number;
87
- style?: string;
88
- }
89
-
90
- /**
91
- * GenerateAttributesResultJsx
92
- * This interface represents the result of the generateAttributes method as JSX
93
- */
94
- export interface GenerateAttributesResultJsx {
95
- fetchPriority: HTMLImageElement['fetchPriority'];
96
- loading: HTMLImageElement['loading'];
97
- decoding: HTMLImageElement['decoding'];
98
- src: string;
99
- srcSet?: string;
100
- sizes?: string;
101
- width?: number;
102
- height?: number;
103
- style?: React.CSSProperties;
104
- }
105
-
106
- /**
107
- * IImageRenderer
108
- * This interface represents the image renderer in charge of generating the attributes for the image element
109
- */
110
- export interface IImageRenderer {
111
- /**
112
- * This method generates the attributes for the image element based on the provided props
113
- * This is the main method that should be used to generate the attributes for the image element
114
- * @param props
115
- */
116
- generateAttributes(props: EcoImageProps): GenerateAttributesResult | null;
117
- /**
118
- * This method generates the attributes for the image element based on the provided props as JSX
119
- * @param props
120
- */
121
- generateAttributesJsx(props: EcoImageProps): GenerateAttributesResultJsx | null;
122
- /**
123
- * This method generates the image element based on the provided props as a string
124
- * @param props
125
- */
126
- renderToString(props: EcoImageProps): string;
127
- }
128
-
129
- export class LayoutAttributesManager {
130
- static shouldIncludeWidthHeight(layout: ImageLayout): boolean {
131
- return layout === 'fixed';
132
- }
133
-
134
- static filterDimensionAttributes(
135
- props: Pick<EcoImageProps, 'width' | 'height' | 'layout'>,
136
- ): Pick<EcoImageProps, 'width' | 'height'> {
137
- const layout = props.layout || 'constrained';
138
-
139
- if (!LayoutAttributesManager.shouldIncludeWidthHeight(layout)) {
140
- return {};
141
- }
142
-
143
- return {
144
- ...(props.width && { width: props.width }),
145
- ...(props.height && { height: props.height }),
146
- };
147
- }
148
-
149
- static getEffectiveDimensions(
150
- props: EcoImageProps,
151
- variants?: Array<{ width: number; height: number }>,
152
- ): { width?: number; height?: number } {
153
- const mainVariant = variants?.[0];
154
- const layout = props.layout || DEFAULT_LAYOUT;
155
-
156
- if (layout === 'constrained' && props.width && !props.height) {
157
- return { width: props.width };
158
- }
159
-
160
- const effectiveWidth = props.width || mainVariant?.width;
161
- let effectiveHeight = props.height || mainVariant?.height;
162
-
163
- if (props.aspectRatio && effectiveWidth) {
164
- const [aspectWidth, aspectHeight] = props.aspectRatio.split('/').map(Number);
165
- effectiveHeight = Math.round((effectiveWidth * aspectHeight) / aspectWidth);
166
- }
167
-
168
- return { width: effectiveWidth, height: effectiveHeight };
169
- }
170
- }
171
-
172
- /**
173
- * ImageRenderer
174
- * This class is responsible for generating the attributes for the image element
175
- */
176
- export class ImageRenderer implements IImageRenderer {
177
- private originalProps?: EcoImageProps;
178
-
179
- private readonly internalProps = [
180
- 'attributes',
181
- 'variants',
182
- 'layout',
183
- 'staticVariant',
184
- 'aspectRatio',
185
- 'unstyled',
186
- 'priority',
187
- 'width',
188
- 'height',
189
- 'cacheKey',
190
- ];
191
-
192
- generateAttributes(props: EcoImageProps): GenerateAttributesResult | null {
193
- this.originalProps = props;
194
- const collected = this.collectAttributes(props);
195
- if (!collected) return null;
196
-
197
- const { styles, width, height, ...rest } = collected;
198
- const dimensions = LayoutAttributesManager.filterDimensionAttributes({
199
- width,
200
- height,
201
- layout: props.layout,
202
- });
203
-
204
- const htmlAttributes = this.getHtmlAttributes(props);
205
- const generatedStyles = styles ? this.generateStyleString(styles) : '';
206
- const userStyles = props.style || '';
207
-
208
- return {
209
- ...htmlAttributes,
210
- ...rest,
211
- ...dimensions,
212
- style: userStyles ? `${generatedStyles};${userStyles}` : generatedStyles,
213
- };
214
- }
215
-
216
- private getHtmlAttributes(props: EcoImageProps): Record<string, any> {
217
- return Object.fromEntries(
218
- Object.entries(props).filter(
219
- ([key]) => !this.internalProps.includes(key) && key !== 'attributes' && key !== 'variants',
220
- ),
221
- );
222
- }
223
-
224
- generateAttributesJsx(props: EcoImageProps): GenerateAttributesResultJsx | null {
225
- this.originalProps = props;
226
- const collected = this.collectAttributes(props);
227
- if (!collected) return null;
228
-
229
- const { styles, fetchpriority, loading, decoding, src, srcset, sizes, width, height } = collected;
230
-
231
- const dimensions = LayoutAttributesManager.filterDimensionAttributes({
232
- width,
233
- height,
234
- layout: props.layout,
235
- });
236
-
237
- const validHtmlAttributes = Object.fromEntries(
238
- Object.entries(props).filter(([key]) => !this.internalProps.includes(key)),
239
- );
240
-
241
- const generatedStyles = styles ? this.createCamelCaseKeysOnStyle(styles) : {};
242
- const userStyles = typeof props.style === 'object' ? props.style : {};
243
-
244
- return {
245
- fetchPriority: fetchpriority,
246
- loading,
247
- decoding,
248
- src,
249
- srcSet: srcset,
250
- sizes,
251
- ...dimensions,
252
- ...validHtmlAttributes,
253
- style: { ...generatedStyles, ...userStyles },
254
- };
255
- }
256
-
257
- renderToString({ attributes, variants, ...rest }: EcoImageProps): string {
258
- this.originalProps = { attributes, variants, ...rest };
259
- const derivedAttributes = this.generateAttributes(this.originalProps);
260
-
261
- if (!derivedAttributes) return '';
262
-
263
- const stringifiedAttributes = this.stringifyAttributes(derivedAttributes);
264
-
265
- return `<img ${stringifiedAttributes} />`;
266
- }
267
-
268
- private collectAttributes(props: EcoImageProps): CollectedAttributes | null {
269
- const { variants, priority, layout = DEFAULT_LAYOUT, unstyled, staticVariant } = props;
270
-
271
- const priorityAttributes = this.getPriorityAttributes(priority);
272
-
273
- if (!variants || variants.length === 0) {
274
- return this.handleNoVariants(props, priorityAttributes);
275
- }
276
-
277
- const mainVariant = this.getMainVariant(variants, staticVariant);
278
- if (!mainVariant) return null;
279
-
280
- const dimensions = this.calculateEffectiveDimensions(props, mainVariant);
281
- const styles = this.calculateStyles({
282
- dimensions,
283
- layout,
284
- attributes: props.attributes,
285
- unstyled,
286
- });
287
-
288
- const imageAttributes = this.buildImageAttributes(mainVariant, dimensions, props, priorityAttributes, styles);
289
-
290
- return imageAttributes;
291
- }
292
-
293
- private getPriorityAttributes(
294
- priority?: boolean,
295
- ): Pick<GenerateAttributesResult, 'loading' | 'fetchpriority' | 'decoding'> {
296
- return {
297
- loading: priority ? 'eager' : 'lazy',
298
- fetchpriority: priority ? 'high' : 'auto',
299
- decoding: priority ? 'auto' : 'async',
300
- };
301
- }
302
-
303
- private getMainVariant(variants: ImageVariant[], staticVariant?: string): ImageVariant | null {
304
- if (staticVariant) {
305
- return variants.find((v) => v.label === staticVariant) || null;
306
- }
307
-
308
- return variants.sort((a, b) => b.width - a.width)[0] || null;
309
- }
310
-
311
- private calculateEffectiveDimensions(
312
- props: EcoImageProps,
313
- variant: ImageVariant,
314
- ): { width?: number; height?: number } {
315
- return LayoutAttributesManager.getEffectiveDimensions(props, [variant]);
316
- }
317
-
318
- private calculateStyles({
319
- dimensions,
320
- layout,
321
- unstyled,
322
- attributes,
323
- }: {
324
- dimensions: { width?: number; height?: number };
325
- layout: ImageLayout;
326
- attributes: ImageSpecifications['attributes'];
327
- unstyled?: boolean;
328
- }): [string, string][] {
329
- if (unstyled) return [];
330
-
331
- return ImageUtils.generateLayoutStyles({
332
- ...dimensions,
333
- layout,
334
- aspectRatio: this.originalProps?.aspectRatio,
335
- attributes,
336
- });
337
- }
338
-
339
- private buildImageAttributes(
340
- variant: ImageVariant,
341
- dimensions: { width?: number; height?: number },
342
- props: EcoImageProps,
343
- priorityAttributes: Pick<GenerateAttributesResult, 'loading' | 'fetchpriority' | 'decoding'>,
344
- styles: [string, string][],
345
- ): CollectedAttributes {
346
- const { staticVariant, attributes } = props;
347
- const useResponsiveImage = !staticVariant;
348
-
349
- return {
350
- ...dimensions,
351
- ...priorityAttributes,
352
- src: variant.src,
353
- ...(useResponsiveImage
354
- ? {
355
- srcset: attributes.srcset,
356
- sizes: attributes.sizes,
357
- }
358
- : {}),
359
- styles,
360
- };
361
- }
362
-
363
- private handleNoVariants(
364
- props: EcoImageProps,
365
- priorityAttributes: Pick<GenerateAttributesResult, 'loading' | 'fetchpriority' | 'decoding'>,
366
- ): CollectedAttributes {
367
- const { attributes, width, height, layout, unstyled } = props;
368
-
369
- const dimensions = {
370
- width,
371
- height,
372
- };
373
-
374
- const styles = this.calculateStyles({
375
- dimensions,
376
- layout: layout || DEFAULT_LAYOUT,
377
- attributes,
378
- unstyled,
379
- });
380
-
381
- return {
382
- ...priorityAttributes,
383
- ...dimensions,
384
- src: attributes.src,
385
- styles,
386
- };
387
- }
388
- private stringifyAttributes(attributes: GenerateAttributesResult): string {
389
- const attributePairs: string[] = [];
390
-
391
- for (const [key, value] of Object.entries(attributes)) {
392
- if (value == null || value === '') continue;
393
-
394
- if (typeof value === 'boolean') {
395
- if (value) attributePairs.push(key);
396
- } else {
397
- attributePairs.push(`${key}="${value}"`);
398
- }
399
- }
400
-
401
- return attributePairs.join(' ');
402
- }
403
-
404
- private generateStyleString(styles: [string, string][]): string {
405
- const styleMap = new Map<string, string>();
406
-
407
- for (const [key, value] of styles) {
408
- const camelKey = key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
409
- styleMap.set(camelKey, value);
410
- }
411
-
412
- return Array.from(styleMap.entries())
413
- .map(([key, value]) => {
414
- const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
415
- return `${cssKey}:${value}`;
416
- })
417
- .join(';');
418
- }
419
-
420
- private createCamelCaseKeysOnStyle(styles: [string, string][]): Record<string, string> {
421
- return Object.fromEntries(
422
- styles.map(([key, value]) => [key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()), value]),
423
- );
424
- }
425
- }
426
-
427
- export const renderer = new ImageRenderer();
@@ -1,128 +0,0 @@
1
- import { DEFAULT_LAYOUT } from './constants';
2
- import type { EcoImageProps } from './image-renderer';
3
-
4
- /**
5
- * ImageUtils
6
- * This class contains utility methods for working with images
7
- * It contains methods for generating srcset, sizes and styles attributes for the image element
8
- */
9
- export class ImageUtils {
10
- private static readonly BREAKPOINTS = {
11
- desktop: 1024,
12
- tablet: 768,
13
- } as const;
14
-
15
- private static readonly VIEWPORT_SIZES = {
16
- desktop: '70vw',
17
- tablet: '80vw',
18
- mobile: '100vw',
19
- } as const;
20
-
21
- static readonly DEFAULT_LAYOUT = DEFAULT_LAYOUT;
22
-
23
- /**
24
- * Generates a srcset string from processed image variants using relative paths
25
- * @param {ImageVariant[]} variants - Array of processed image variants
26
- * @returns {string} srcset attribute string
27
- * @private
28
- */
29
- static generateSrcset(variants: Array<{ width: number; src: string }>): string {
30
- return variants
31
- .sort((a, b) => b.width - a.width)
32
- .map(({ src, width }) => `${src} ${width}w`)
33
- .join(', ');
34
- }
35
-
36
- /**
37
- * Generates sizes attribute based on image variants.
38
- * Sizes are generated based on the variant widths and breakpoints.
39
- * Here we use a smart approach to generate sizes based on the variant widths.
40
- * We start with the largest variant and set a min-width condition for its width.
41
- * Then we add conditions for each variant based on the viewport width.
42
- * Finally, we add a catch-all for the smallest screens.
43
- * This approach ensures that the browser will select the correct image variant based on the viewport width.
44
- * @see https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images#resolution_switching_different_sizes
45
- * @param {ImageVariant[]} variants - Array of processed image variants
46
- * @returns {string} sizes attribute string
47
- * @private
48
- */
49
- static generateSizes(variants: Array<{ width: number }>): string {
50
- if (!variants?.length) return '';
51
-
52
- const sortedVariants = [...variants].sort((a, b) => b.width - a.width);
53
- const [largest, ...rest] = sortedVariants;
54
-
55
- const conditions = [
56
- `(min-width: ${largest.width}px) ${largest.width}px`,
57
- ...rest
58
- .map((variant) => {
59
- if (variant.width >= ImageUtils.BREAKPOINTS.desktop) {
60
- return `(min-width: ${variant.width}px) ${ImageUtils.VIEWPORT_SIZES.desktop}`;
61
- }
62
- if (variant.width >= ImageUtils.BREAKPOINTS.tablet) {
63
- return `(min-width: ${variant.width}px) ${ImageUtils.VIEWPORT_SIZES.tablet}`;
64
- }
65
- return null;
66
- })
67
- .filter(Boolean),
68
- ImageUtils.VIEWPORT_SIZES.mobile,
69
- ];
70
-
71
- return conditions.join(', ');
72
- }
73
-
74
- /**
75
- * Generates a styles string based on image layout and config
76
- * @param {ImageLayout} layout - Image layout type
77
- * @param {LayoutAttributes} config - Image layout configuration
78
- * @returns {string} styles string
79
- * @private
80
- */
81
- static generateLayoutStyles(
82
- config: Pick<EcoImageProps, 'layout' | 'width' | 'height' | 'aspectRatio' | 'attributes'>,
83
- ): [string, string][] {
84
- const layout = config.layout || ImageUtils.DEFAULT_LAYOUT;
85
- const styles: [string, string][] = [['object-fit', 'cover']];
86
-
87
- if (config.aspectRatio) {
88
- styles.push(['aspect-ratio', config.aspectRatio]);
89
- }
90
-
91
- switch (layout) {
92
- case 'fixed':
93
- if (config.width) {
94
- styles.push(['width', `${config.width}px`], ['min-width', `${config.width}px`]);
95
- }
96
- if (config.height) {
97
- styles.push(['height', `${config.height}px`], ['min-height', `${config.height}px`]);
98
- }
99
- break;
100
-
101
- case 'constrained':
102
- styles.push(['width', '100%']);
103
-
104
- if (config.width && config.height) {
105
- styles.push(['max-width', `${config.width}px`], ['max-height', `${config.height}px`]);
106
- if (!config.aspectRatio) {
107
- styles.push(['aspect-ratio', `${config.width}/${config.height}`]);
108
- }
109
- } else if (config.height) {
110
- styles.push(['height', `${config.height}px`], ['min-height', `${config.height}px`]);
111
- } else if (config.width) {
112
- styles.push(['max-width', `${config.width}px`]);
113
- } else if (!config.aspectRatio) {
114
- styles.push(['aspect-ratio', `${config.attributes.width}/${config.attributes.height}`]);
115
- }
116
- break;
117
-
118
- case 'full-width':
119
- styles.push(['width', '100%']);
120
- if (config.height) {
121
- styles.push(['height', `${config.height}px`], ['min-height', `${config.height}px`]);
122
- }
123
- break;
124
- }
125
-
126
- return styles;
127
- }
128
- }
package/src/index.ts DELETED
@@ -1,3 +0,0 @@
1
- export * from './image-processor';
2
- export * from './plugin';
3
- export * from './types';