next-cloudinary 6.17.4 → 6.18.6

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.
@@ -0,0 +1,698 @@
1
+ /**
2
+ * Code generation config for next-cloudinary.
3
+ * The CSS service reads this file to generate <CldImage> / <CldVideoPlayer> JSX snippets
4
+ * for the Cloudinary documentation pages.
5
+ *
6
+ * customGenerator(mediaConfig, includeImports) => string
7
+ * - mediaConfig.actionsDTO - structured transformation data from Ruby CGS
8
+ * - mediaConfig.transformation - raw parsed_url transformation array (always populated)
9
+ * - mediaConfig.public_id - Cloudinary public ID
10
+ * - mediaConfig.resource_type - "image" | "video"
11
+ * - includeImports - whether to prepend the import statement
12
+ *
13
+ * Fallback strategy:
14
+ * 1. actionsDTO → named JSX props for all known transformations
15
+ * 2. mediaConfig.transformation → generative AI effects (Ruby CGS ≤0.1.84 returns
16
+ * actions:null for e_gen_* — newer versions do return them, but this fallback
17
+ * makes local dev work against the older CGS docker image too)
18
+ * 3. Unknown effect/adjust groups → rawTransformations["e_{name}:{level}"]
19
+ * NOTE: rawTransformations reconstruction is correct for Cloudinary e_* params only.
20
+ * Parameters with a different URL prefix (e.g. o_ for opacity) must be in
21
+ * EFFECT_PROP_MAP to avoid generating an incorrect raw string.
22
+ */
23
+
24
+ // ─── Effect map ──────────────────────────────────────────────────────────────
25
+ // Cloudinary internal effect name → CldImage prop name.
26
+ // Anything NOT in this map goes to rawTransformations.
27
+ var EFFECT_PROP_MAP = {
28
+ sepia: 'sepia',
29
+ grayscale: 'grayscale',
30
+ // blur/blur_faces/blur_region: actionsDTO always uses name "blur" with a
31
+ // region qualifier to distinguish variants — handled by special case below.
32
+ blur: 'blur',
33
+ // e_viesus_correct has no CldImage prop — goes to rawTransformations as e_viesus_correct.
34
+ improve: 'improve',
35
+ sharpen: 'sharpen',
36
+ brightness: 'brightness',
37
+ brightness_hsb: 'brightnessHSB',
38
+ contrast: 'contrast',
39
+ saturation: 'saturation',
40
+ vibrance: 'vibrance',
41
+ shadow: 'shadow',
42
+ colorize: 'colorize',
43
+ // opacity uses o_{level} in Cloudinary URLs — must be in this map so we emit
44
+ // `opacity={50}` rather than the incorrect `rawTransformations={["e_opacity:50"]}`
45
+ opacity: 'opacity',
46
+ // pixelate/pixelate_faces/pixelate_region: actionsDTO always uses name "pixelate"
47
+ // with a region qualifier — handled by special case below.
48
+ pixelate: 'pixelate',
49
+ oil_paint: 'oilPaint',
50
+ cartoonify: 'cartoonify',
51
+ outline: 'outline',
52
+ blackwhite: 'blackwhite',
53
+ negate: 'negate',
54
+ // e_redeye → actionsDTO type "red_eye"
55
+ red_eye: 'redeye',
56
+ background_removal: 'removeBackground',
57
+ remove_background: 'removeBackground',
58
+ auto_brightness: 'autoBrightness',
59
+ auto_color: 'autoColor',
60
+ auto_contrast: 'autoContrast',
61
+ assist_colorblind: 'assistColorblind',
62
+ assist_color_blind: 'assistColorblind',
63
+ simulate_colorblind: 'simulateColorblind',
64
+ simulate_color_blind: 'simulateColorblind',
65
+ fill_light: 'fillLight',
66
+ gamma: 'gamma',
67
+ gradient_fade: 'gradientFade',
68
+ multiply: 'multiply',
69
+ replace_color: 'replaceColor',
70
+ screen: 'screen',
71
+ tint: 'tint',
72
+ trim: 'trim',
73
+ unsharp_mask: 'unsharpMask',
74
+ vectorize: 'vectorize',
75
+ vignette: 'vignette',
76
+ // e_art:style → actionsDTO type "artistic_filter", handled by special case below
77
+ artistic_filter: 'art',
78
+ hue: 'hue',
79
+ noise: 'noise',
80
+ // e_loop without parameters → actionsDTO group "animated" — handled by special case below
81
+ // e_loop:2,f_mp4 → actionsDTO group "effect" name "loop" (different code path)
82
+ loop: 'loop',
83
+ shear: 'shear',
84
+ enhance: 'enhance',
85
+ zoompan: 'zoompan',
86
+ displace: 'displace',
87
+ };
88
+
89
+ // actionsDTO internal effect names that differ from Cloudinary URL parameter names.
90
+ // Used in the rawTransformations fallback to emit the correct e_{urlName} string.
91
+ var EFFECT_URL_NAME_OVERRIDE = {
92
+ // actionsDTO "advanced_red_eye" → URL "e_adv_redeye"
93
+ advanced_red_eye: 'adv_redeye',
94
+ };
95
+
96
+ // actionsDTO uses internal gravity type names; normalize to Cloudinary URL values
97
+ var GRAVITY_NORMALIZE = {
98
+ auto_gravity: 'auto',
99
+ };
100
+
101
+ // actionsDTO focus subject names (inside autoGravity) → Cloudinary URL focus values.
102
+ // e.g. CGS actionsDTO uses "ocr" but Cloudinary URL is g_auto:ocr_text.
103
+ var GRAVITY_FOCUS_NORMALIZE = {
104
+ ocr: 'ocr_text',
105
+ };
106
+
107
+ // actionsDTO background types that the CGS strips the "auto:" prefix from.
108
+ // e.g. "b_auto:predominant" → actionsDTO name "predominant" → URL needs "auto:predominant".
109
+ var BACKGROUND_URL_NAME = {
110
+ predominant: 'auto:predominant',
111
+ predominant_gradient: 'auto:predominant_gradient',
112
+ border: 'auto:border',
113
+ border_gradient: 'auto:border_gradient',
114
+ };
115
+
116
+ // actionsDTO uses internal SDK crop mode names that differ from the URL shorthand
117
+ // codes accepted by CldImage's crop prop (which match the Cloudinary URL params).
118
+ var CROP_MODE_NORMALIZE = {
119
+ limit_fill: 'lfill',
120
+ limit_pad: 'lpad',
121
+ minimum_fit: 'mfit',
122
+ minimum_pad: 'mpad',
123
+ thumbnail: 'thumb',
124
+ // limit_fit (c_lfit) has no direct equivalent in the url-loader schema; omit
125
+ // so it falls through as-is rather than silently producing an invalid value.
126
+ };
127
+
128
+ // ─── actionsDTO helpers ───────────────────────────────────────────────────────
129
+
130
+ /**
131
+ * Walk a qualifier's .qualifiers chain to return the scalar leaf value.
132
+ * Handles simple scalars (width, height, level).
133
+ */
134
+ function resolveQualifierValue(q) {
135
+ if (!q) return undefined;
136
+ if (q.value !== undefined) return q.value;
137
+ if (q.qualifiers && q.qualifiers.length > 0) {
138
+ var leaf = q.qualifiers[0];
139
+ while (leaf.qualifiers && leaf.qualifiers.length > 0) {
140
+ leaf = leaf.qualifiers[0];
141
+ }
142
+ return leaf.name;
143
+ }
144
+ return undefined;
145
+ }
146
+
147
+ /**
148
+ * Resolve the gravity qualifier, reconstructing compound auto gravity strings.
149
+ *
150
+ * Simple gravity (e.g. g_faces):
151
+ * { name:"gravity", qualifiers:[{ name:"faces" }] } → "faces"
152
+ *
153
+ * Compound auto gravity (e.g. g_auto:face):
154
+ * { name:"gravity", qualifiers:[{ name:"auto_gravity", qualifiers:[
155
+ * { name:"auto_focus", qualifiers:[{ name:"focus_on", qualifiers:[{ name:"face" }] }] }
156
+ * ]}] } → "auto:face"
157
+ */
158
+ function resolveGravityValue(qualifiers) {
159
+ var gravityQ = qualifiers.find(function(q) { return q.name === 'gravity'; });
160
+ if (!gravityQ || !gravityQ.qualifiers || !gravityQ.qualifiers.length) return undefined;
161
+
162
+ var typeQual = gravityQ.qualifiers[0]; // e.g. { name: "auto_gravity", qualifiers: [...] }
163
+ var typeName = GRAVITY_NORMALIZE[typeQual.name] || typeQual.name; // "auto" or e.g. "faces"
164
+
165
+ // Simple gravity — no nested focus qualifiers
166
+ if (!typeQual.qualifiers || !typeQual.qualifiers.length) return typeName;
167
+
168
+ // Compound auto gravity — walk to the leaf focus subject name
169
+ var leaf = typeQual.qualifiers[0];
170
+ while (leaf.qualifiers && leaf.qualifiers.length > 0) {
171
+ leaf = leaf.qualifiers[0];
172
+ }
173
+ var focusName = GRAVITY_FOCUS_NORMALIZE[leaf.name] || leaf.name;
174
+ return typeName === 'auto' && focusName ? 'auto:' + focusName : (focusName || typeName);
175
+ }
176
+
177
+ function makeGetQualifier(qualifiers) {
178
+ return function(qualName) {
179
+ var q = qualifiers.find(function(q) { return q.name === qualName; });
180
+ return resolveQualifierValue(q);
181
+ };
182
+ }
183
+
184
+ // ─── Color qualifier helper ───────────────────────────────────────────────────
185
+ //
186
+ // Extracts a Cloudinary-compatible color string from an actionsDTO color qualifier.
187
+ // Simple color: { group:"color", name:"red" } → "red"
188
+ // RGB hex: { group:"color", name:"rgb", qualifiers:[{ name:"color", value:"#9090ff" }] }
189
+ // → "rgb:9090ff" (hash stripped — Cloudinary URL format)
190
+ //
191
+ function extractColorString(qual) {
192
+ if (!qual || qual.group !== 'color') return undefined;
193
+ if (qual.name === 'rgb' && qual.qualifiers && qual.qualifiers.length > 0) {
194
+ var hex = qual.qualifiers[0].value;
195
+ if (hex) return 'rgb:' + String(hex).replace('#', '');
196
+ }
197
+ return qual.name || undefined;
198
+ }
199
+
200
+ // ─── actionsDTO → CldImage props ─────────────────────────────────────────────
201
+
202
+ function mapActionsDTOToImageProps(actionsDTO) {
203
+ var props = {};
204
+ var rawTransformations = [];
205
+
206
+ (actionsDTO || []).forEach(function(action) {
207
+ var group = (action.group || '').toLowerCase();
208
+ var name = (action.name || '').toLowerCase();
209
+ var qualifiers = action.qualifiers || [];
210
+ var getQualifier = makeGetQualifier(qualifiers);
211
+
212
+ if (group === 'resize') {
213
+ var width = getQualifier('width');
214
+ var height = getQualifier('height');
215
+ var gravity = resolveGravityValue(qualifiers);
216
+ var aspectRatio = getQualifier('aspect_ratio');
217
+ var zoom = getQualifier('zoom');
218
+
219
+ var x = getQualifier('x');
220
+ var y = getQualifier('y');
221
+
222
+ // actionsDTO uses internal SDK names (e.g. "limit_fill", "thumbnail") while
223
+ // CldImage's crop prop expects the URL shorthand forms ("lfill", "thumb").
224
+ var cropMode = CROP_MODE_NORMALIZE[name] || name;
225
+
226
+ // b_auto / b_blurred in the same step as c_pad appear as an embedded
227
+ // background qualifier: { name:"background", qualifiers:[{group:"background",name:"auto"}] }
228
+ var bgQual = qualifiers.find(function(q) { return q.name === 'background'; });
229
+ if (bgQual && bgQual.qualifiers && bgQual.qualifiers.length > 0) {
230
+ var innerBg = bgQual.qualifiers[0]; // {group:"background", name:"auto"|"blurred"|...}
231
+ if (innerBg && innerBg.group === 'background') {
232
+ // b_gen_fill embedded in a resize step is represented as
233
+ // background.color("gen_fill") in actionsDTO (CGS quirk — the CGS treats
234
+ // gen_fill as a color value rather than a generative fill background type).
235
+ // Detect this pattern and emit fillBackground instead of background.
236
+ var innerIsGenFill = innerBg.name === 'color' &&
237
+ innerBg.qualifiers && innerBg.qualifiers.length > 0 &&
238
+ innerBg.qualifiers[0].name === 'gen_fill';
239
+ if (innerIsGenFill) {
240
+ if (!props.fillBackground) props.fillBackground = true;
241
+ } else {
242
+ var bgUrlName = BACKGROUND_URL_NAME[innerBg.name] || innerBg.name;
243
+ var bgStr = bgUrlName;
244
+ if (innerBg.qualifiers && innerBg.qualifiers.length > 0) {
245
+ var bgParams = innerBg.qualifiers.map(function(q) {
246
+ return resolveQualifierValue(q);
247
+ }).filter(function(v) { return v !== undefined && v !== null; });
248
+ if (bgParams.length > 0) bgStr += ':' + bgParams.join(':');
249
+ }
250
+ if (bgStr) props.background = bgStr;
251
+ }
252
+ }
253
+ }
254
+
255
+ if (width !== undefined) props.width = width;
256
+ if (height !== undefined) props.height = height;
257
+ if (cropMode !== 'scale') props.crop = cropMode;
258
+ if (gravity !== undefined) props.gravity = gravity;
259
+ if (aspectRatio !== undefined) props.aspectRatio = aspectRatio;
260
+ if (zoom !== undefined) props.zoom = zoom;
261
+ if (x !== undefined) props.x = x;
262
+ if (y !== undefined) props.y = y;
263
+
264
+ } else if (group === 'effect' || group === 'adjust' || group === 'animated' || group === 'reshape') {
265
+ // --- Special cases where actionsDTO internal structure needs custom handling ---
266
+
267
+ if (name === 'artistic_filter') {
268
+ // e_art:al_dente → actionsDTO: {group:"effect", name:"artistic_filter",
269
+ // qualifiers:[{group:"artistic_filter", name:"al_dente"}]}
270
+ // The art style name lives in qualifiers[0].name.
271
+ var artStyle = qualifiers.length > 0 ? qualifiers[0].name : undefined;
272
+ if (artStyle) props.art = artStyle;
273
+
274
+ } else if (name === 'blur') {
275
+ // e_blur, e_blur_faces, e_blur_region all produce name:"blur" in actionsDTO.
276
+ // The "region" qualifier (type "faces", "ocr", "custom", …) distinguishes them.
277
+ // blur uses "strength" qualifier, not "level".
278
+ var blurStrength = getQualifier('strength') || getQualifier('level');
279
+ var blurRegion = getQualifier('region');
280
+ if (blurRegion === 'faces') {
281
+ props.blurFaces = blurStrength !== undefined ? blurStrength : true;
282
+ } else if (blurRegion !== undefined) {
283
+ props.blurRegion = blurStrength !== undefined ? blurStrength : true;
284
+ } else {
285
+ props.blur = blurStrength !== undefined ? blurStrength : true;
286
+ }
287
+
288
+ } else if (name === 'pixelate') {
289
+ // e_pixelate, e_pixelate_faces, e_pixelate_region all produce name:"pixelate".
290
+ // Level qualifier is "square_size" (not "level"/"strength").
291
+ var squareSize = getQualifier('square_size') || getQualifier('level');
292
+ var pixRegion = getQualifier('region');
293
+ if (pixRegion === 'faces') {
294
+ props.pixelateFaces = squareSize !== undefined ? squareSize : true;
295
+ } else if (pixRegion !== undefined) {
296
+ props.pixelateRegion = squareSize !== undefined ? squareSize : true;
297
+ } else {
298
+ props.pixelate = squareSize !== undefined ? squareSize : true;
299
+ }
300
+
301
+ } else if (name === 'distort') {
302
+ // e_distort:40:25:280:60:260:155:35:165 → actionsDTO:
303
+ // {group:"reshape", name:"distort", qualifiers:[{name:"distort_values", value:[40,25,...]}]}
304
+ // url-loader distort prop accepts a colon-separated coordinate string.
305
+ var distortValQual = qualifiers.find(function(q) { return q.name === 'distort_values'; });
306
+ if (distortValQual && Array.isArray(distortValQual.value)) {
307
+ props.distort = distortValQual.value.join(':');
308
+ }
309
+
310
+ } else if (name === 'distort_arc') {
311
+ // e_distort:arc:80 → actionsDTO: {group:"reshape", name:"distort_arc",
312
+ // qualifiers:[{name:"degree", value:80}]}
313
+ // url-loader distort prop accepts "arc:80".
314
+ var arcDegree = getQualifier('degree');
315
+ if (arcDegree !== undefined) {
316
+ props.distort = 'arc:' + arcDegree;
317
+ }
318
+
319
+ } else if (name === 'shear') {
320
+ // e_shear:20 → actionsDTO: {group:"reshape", name:"shear",
321
+ // qualifiers:[{name:"skew_x", value:20}]}
322
+ // e_shear:20:0 → qualifiers:[{name:"skew_x",...},{name:"skew_y",...}]
323
+ // url-loader shear prop accepts the value string "skewX" or "skewX:skewY".
324
+ var skewX = getQualifier('skew_x');
325
+ var skewY = getQualifier('skew_y');
326
+ if (skewX !== undefined) {
327
+ props.shear = skewY !== undefined ? String(skewX) + ':' + String(skewY) : String(skewX);
328
+ }
329
+
330
+ } else if (name === 'replace_color') {
331
+ // e_replace_color:red → actionsDTO: {group:"adjust", name:"replace_color",
332
+ // qualifiers:[{group:"color", name:"red"}]}
333
+ // url-loader replaceColor prop accepts a string (the part after e_replace_color:).
334
+ var toColorQual = qualifiers.find(function(q) { return q.group === 'color'; });
335
+ var toColor = extractColorString(toColorQual);
336
+ if (toColor) props.replaceColor = toColor;
337
+
338
+ } else if (name === 'edit' && group === 'animated') {
339
+ // e_loop / e_loop:5 → actionsDTO: {group:"animated", name:"edit",
340
+ // qualifiers:[{name:"loop", value:true|5}]}
341
+ var loopQ = qualifiers.find(function(q) { return q.name === 'loop'; });
342
+ if (loopQ) {
343
+ var loopVal = resolveQualifierValue(loopQ);
344
+ props.loop = (loopVal !== undefined && loopVal !== true) ? loopVal : true;
345
+ }
346
+
347
+ } else {
348
+ // 'blend' used by fill_light and improve; 'line_strength' used by cartoonify.
349
+ var level = getQualifier('level') || getQualifier('strength') || getQualifier('blend') || getQualifier('line_strength');
350
+ var propName = EFFECT_PROP_MAP[name];
351
+ if (propName) {
352
+ props[propName] = level !== undefined ? level : true;
353
+ } else {
354
+ var urlName = EFFECT_URL_NAME_OVERRIDE[name] || name;
355
+ rawTransformations.push(level !== undefined ? 'e_' + urlName + ':' + level : 'e_' + urlName);
356
+ }
357
+ }
358
+
359
+ } else if (group === 'delivery') {
360
+ if (name === 'quality') {
361
+ var qLevel = getQualifier('level') || getQualifier('quality');
362
+ props.quality = qLevel !== undefined ? qLevel : 'auto';
363
+ }
364
+
365
+ } else if (group === 'round_corners') {
366
+ // r_20 → actionsDTO: {group:"round_corners", name:"by_radius",
367
+ // qualifiers:[{name:"radius", value:20}]}
368
+ // r_max → actionsDTO: {group:"round_corners", name:"max"}
369
+ if (name === 'by_radius') {
370
+ var radiusVal = getQualifier('radius');
371
+ if (radiusVal !== undefined) props.radius = radiusVal;
372
+ } else if (name) {
373
+ props.radius = name; // e.g. "max"
374
+ }
375
+
376
+ } else if (name === 'background_color') {
377
+ // background_color has no group in actionsDTO.
378
+ // b_red → qualifiers:[{group:"color", name:"red"}]
379
+ // b_rgb:9090ff → qualifiers:[{group:"color", name:"rgb", qualifiers:[{name:"color",value:"#9090ff"}]}]
380
+ var bgColor = extractColorString(qualifiers[0]);
381
+ if (bgColor) props.background = bgColor;
382
+
383
+ } else if (group === 'rotate') {
384
+ // angle uses a_ URL prefix (not e_), so must be handled here, not via rawTransformations.
385
+ // actionsDTO qualifier name is 'angle' matching the human-readable parameter name.
386
+ var angleVal = getQualifier('angle');
387
+ if (angleVal !== undefined) props.angle = angleVal;
388
+
389
+ } else if (group === 'border') {
390
+ // Ruby CGS: { group:"border", name:"solid", qualifiers:[{name:"width",value:5},{name:"color",...}] }
391
+ // CldImage border prop accepts a string like "5px_solid_black" (same as URL sans bo_ prefix).
392
+ var borderWidth = getQualifier('width');
393
+ var borderColor = getQualifier('color');
394
+ var parts = [];
395
+ if (borderWidth !== undefined) parts.push(borderWidth + 'px');
396
+ if (name) parts.push(name); // style: "solid"
397
+ if (borderColor !== undefined) parts.push(borderColor);
398
+ if (parts.length > 0) props.border = parts.join('_');
399
+
400
+ } else if (name === 'add_flag') {
401
+ // add_flag is NOT unwrapped by convertAction (special case alongside background_color).
402
+ // Named flag: { name:"add_flag", qualifiers:[{group:"flag", name:"progressive"}] }
403
+ // Raw flag: { name:"add_flag", qualifiers:[{name:"flag", value:"unhandled"}] }
404
+ // → for raw flags the qualifier name is "flag" and the value holds the actual flag string.
405
+ var flagQual = qualifiers[0];
406
+ if (flagQual) {
407
+ var flagName = (flagQual.name === 'flag' && flagQual.value !== undefined)
408
+ ? flagQual.value
409
+ : flagQual.name;
410
+ if (flagName) {
411
+ if (!props.flags) props.flags = [];
412
+ props.flags.push(flagName);
413
+ }
414
+ }
415
+
416
+ } else if (group === 'named_transformation') {
417
+ // t_my_preset → actionsDTO: {group:"named_transformation", name:"name",
418
+ // qualifiers:[{name:"named_transformation", value:"my_preset"}]}
419
+ // The preset name is in the qualifier whose name is "named_transformation".
420
+ var transformName = getQualifier('named_transformation');
421
+ if (transformName) {
422
+ if (!props.namedTransformations) props.namedTransformations = [];
423
+ props.namedTransformations.push(transformName);
424
+ }
425
+
426
+ } else if (group === 'background') {
427
+ // b_auto → {group:"background", name:"auto"}
428
+ // b_blurred:400:15 → {group:"background", name:"blurred",
429
+ // qualifiers:[{name:"intensity",value:400},{name:"brightness",value:15}]}
430
+ // b_gen_fill:prompt_mug → {group:"background", name:"generative_fill",
431
+ // qualifiers:[{name:"prompt",value:"mug"}]}
432
+ if (name === 'generative_fill') {
433
+ if (!props.fillBackground) {
434
+ var genFillPrompt = getQualifier('prompt');
435
+ props.fillBackground = genFillPrompt ? { prompt: genFillPrompt } : true;
436
+ }
437
+ } else {
438
+ // actionsDTO strips the "auto:" prefix from predominant/border variants:
439
+ // b_auto:predominant → name "predominant", b_auto:border → name "border".
440
+ // BACKGROUND_URL_NAME restores the correct URL form.
441
+ var bgUrlName = BACKGROUND_URL_NAME[name] || name;
442
+ var bgStr = bgUrlName;
443
+ var bgParams = qualifiers.map(function(q) {
444
+ return resolveQualifierValue(q);
445
+ }).filter(function(v) { return v !== undefined && v !== null; });
446
+ if (bgParams.length > 0) bgStr += ':' + bgParams.join(':');
447
+ props.background = bgStr;
448
+ }
449
+ }
450
+ });
451
+
452
+ if (rawTransformations.length > 0) props.rawTransformations = rawTransformations;
453
+ return props;
454
+ }
455
+
456
+ // ─── Overlay/underlay fallback (mediaConfig.transformation) ──────────────────
457
+ //
458
+ // Overlay/underlay actions arrive in actionsDTO as a single grouped action
459
+ // ({ group: "overlay" }) with all source/position data nested inside.
460
+ // We can't reconstruct the full prop form without a stateful multi-action
461
+ // system, so we fall back to rawTransformations using the structured
462
+ // mediaConfig.transformation array (which has the original step-by-step data).
463
+ //
464
+ // Step format: { overlay: "public_id" }, { crop_mode: "scale", width: "100" },
465
+ // { flags: "layer_apply", gravity: "south_east" }
466
+ //
467
+ var TRANSFORMATION_KEY_TO_PREFIX = {
468
+ crop_mode: 'c_', width: 'w_', height: 'h_', gravity: 'g_',
469
+ aspect_ratio: 'ar_', zoom: 'z_', effect: 'e_', quality: 'q_',
470
+ format: 'f_', flags: 'fl_', overlay: 'l_', underlay: 'u_',
471
+ angle: 'a_', x: 'x_', y: 'y_', radius: 'r_', opacity: 'o_',
472
+ border: 'bo_', background: 'b_', color: 'co_', dpr: 'dpr_', delay: 'dl_',
473
+ };
474
+
475
+ function serializeTransformationStep(step) {
476
+ return Object.keys(step).map(function(key) {
477
+ var prefix = TRANSFORMATION_KEY_TO_PREFIX[key];
478
+ if (!prefix) return null;
479
+ var value = step[key];
480
+ if (value === null || value === undefined) return null;
481
+ if (typeof value === 'object') value = Object.keys(value)[0] + ':' + Object.values(value)[0];
482
+ return prefix + value;
483
+ }).filter(Boolean).join(',');
484
+ }
485
+
486
+ function applyOverlayTransformations(transformation, props) {
487
+ var steps = transformation || [];
488
+ var i = 0;
489
+ while (i < steps.length) {
490
+ var step = steps[i];
491
+ var isOverlay = step.overlay !== undefined;
492
+ var isUnderlay = step.underlay !== undefined;
493
+
494
+ if (isOverlay || isUnderlay) {
495
+ var block = [];
496
+ while (i < steps.length) {
497
+ block.push(steps[i]);
498
+ var isLayerApply = steps[i].flags &&
499
+ steps[i].flags.toString().indexOf('layer_apply') !== -1;
500
+ i++;
501
+ if (isLayerApply) break;
502
+ }
503
+ var rawStr = block.map(serializeTransformationStep).filter(Boolean).join('/');
504
+ if (rawStr) {
505
+ if (!props.rawTransformations) props.rawTransformations = [];
506
+ props.rawTransformations.push(rawStr);
507
+ }
508
+ } else {
509
+ i++;
510
+ }
511
+ }
512
+ }
513
+
514
+ // ─── Generative AI fallback (mediaConfig.transformation) ─────────────────────
515
+ //
516
+ // Ruby CGS ≤0.1.84 returns actions:null for e_gen_* transformations, leaving
517
+ // actionsDTO empty. parsed_url.transformation always contains the raw data.
518
+ // This function reads that array and maps known gen_ effects to CldImage props.
519
+ //
520
+ // Transformation step format: { effect: "gen_fill" }
521
+ // { effect: "gen_remove:prompt_tree" }
522
+ // { effect: "gen_replace:from_cat;to_dog" }
523
+ //
524
+ function applyGenerativeEffects(transformation, props) {
525
+ (transformation || []).forEach(function(step) {
526
+ var effect = step.effect;
527
+ if (!effect || typeof effect !== 'string') return;
528
+
529
+ var colonIdx = effect.indexOf(':');
530
+ var name = colonIdx === -1 ? effect : effect.slice(0, colonIdx);
531
+ var paramStr = colonIdx === -1 ? '' : effect.slice(colonIdx + 1);
532
+
533
+ // Parse semicolon-delimited key_value pairs (e.g. "prompt_tree;multiple_true")
534
+ function parseParam(key) {
535
+ var prefix = key + '_';
536
+ var entry = paramStr.split(';').find(function(p) { return p.startsWith(prefix); });
537
+ return entry ? entry.slice(prefix.length) : undefined;
538
+ }
539
+
540
+ if (name === 'gen_fill') {
541
+ if (!props.fillBackground) { // don't overwrite if already set via actionsDTO
542
+ var prompt = parseParam('prompt');
543
+ props.fillBackground = prompt ? { prompt: prompt } : true;
544
+ }
545
+ } else if (name === 'gen_remove') {
546
+ if (!props.remove) {
547
+ var prompt = parseParam('prompt');
548
+ if (prompt) props.remove = { prompt: prompt };
549
+ else props.remove = true;
550
+ }
551
+ } else if (name === 'gen_replace') {
552
+ if (!props.replace) {
553
+ var from = parseParam('from');
554
+ var to = parseParam('to');
555
+ if (from && to) props.replace = [from, to];
556
+ }
557
+ } else if (name === 'gen_recolor') {
558
+ if (!props.recolor) {
559
+ var prompt = parseParam('prompt');
560
+ var toColor = parseParam('to-color');
561
+ if (prompt) props.recolor = toColor ? [prompt, toColor] : { prompt: prompt };
562
+ }
563
+ } else if (name === 'gen_background_replace') {
564
+ if (!props.replaceBackground) {
565
+ var prompt = parseParam('prompt');
566
+ props.replaceBackground = prompt || true;
567
+ }
568
+ } else if (name === 'restore') {
569
+ if (!props.restore) props.restore = true;
570
+ } else if (name === 'enhance') {
571
+ if (!props.enhance) props.enhance = true;
572
+ }
573
+ });
574
+ }
575
+
576
+ // ─── actionsDTO → CldVideoPlayer props ───────────────────────────────────────
577
+ //
578
+ // CldVideoPlayer differs from CldImage:
579
+ // - width and height are direct props (player dimensions)
580
+ // - crop/gravity go into a `transformation` object
581
+ // - quality is a direct prop
582
+
583
+ function mapActionsDTOToVideoProps(actionsDTO) {
584
+ var directProps = {};
585
+ var transformation = {};
586
+
587
+ (actionsDTO || []).forEach(function(action) {
588
+ var group = (action.group || '').toLowerCase();
589
+ var name = (action.name || '').toLowerCase();
590
+ var qualifiers = action.qualifiers || [];
591
+ var getQualifier = makeGetQualifier(qualifiers);
592
+
593
+ if (group === 'resize') {
594
+ var width = getQualifier('width');
595
+ var height = getQualifier('height');
596
+ var gravity = resolveGravityValue(qualifiers);
597
+
598
+ if (width !== undefined) directProps.width = width;
599
+ if (height !== undefined) directProps.height = height;
600
+ if (name !== 'scale') transformation.crop = name;
601
+ if (gravity !== undefined) transformation.gravity = gravity;
602
+
603
+ } else if (group === 'delivery') {
604
+ if (name === 'quality') {
605
+ var qLevel = getQualifier('level') || getQualifier('quality');
606
+ directProps.quality = qLevel !== undefined ? qLevel : 'auto';
607
+ }
608
+ }
609
+ });
610
+
611
+ if (Object.keys(transformation).length > 0) directProps.transformation = transformation;
612
+ return directProps;
613
+ }
614
+
615
+ // ─── JSX builders ─────────────────────────────────────────────────────────────
616
+
617
+ function serializePropValue(value) {
618
+ if (typeof value === 'string') return '"' + value + '"';
619
+ if (typeof value === 'number' || typeof value === 'boolean') return '{' + value + '}';
620
+ if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
621
+ return '{' + JSON.stringify(value) + '}';
622
+ }
623
+ return '{' + String(value) + '}';
624
+ }
625
+
626
+ var IMAGE_ORDERED_PROPS = ['width', 'height', 'crop', 'gravity', 'aspectRatio', 'zoom', 'x', 'y'];
627
+ var VIDEO_ORDERED_PROPS = ['width', 'height', 'quality'];
628
+
629
+ function emitProps(lines, props, orderedKeys) {
630
+ function emitProp(key, value) {
631
+ lines.push(value === true ? ' ' + key : ' ' + key + '=' + serializePropValue(value));
632
+ }
633
+ orderedKeys.forEach(function(key) {
634
+ if (props[key] !== undefined) emitProp(key, props[key]);
635
+ });
636
+ Object.keys(props).forEach(function(key) {
637
+ if (orderedKeys.indexOf(key) !== -1 || key === 'rawTransformations') return;
638
+ emitProp(key, props[key]);
639
+ });
640
+ if (props.rawTransformations !== undefined) {
641
+ lines.push(' rawTransformations=' + serializePropValue(props.rawTransformations));
642
+ }
643
+ }
644
+
645
+ function buildCldImageJSX(publicId, props) {
646
+ var lines = ['<CldImage'];
647
+ lines.push(' src="' + publicId + '"');
648
+ emitProps(lines, props, IMAGE_ORDERED_PROPS);
649
+ lines.push(' alt="Image description"');
650
+ lines.push('/>');
651
+ return lines.join('\n');
652
+ }
653
+
654
+ function buildCldVideoPlayerJSX(publicId, props) {
655
+ var lines = ['<CldVideoPlayer'];
656
+ lines.push(' src="' + publicId + '"');
657
+ emitProps(lines, props, VIDEO_ORDERED_PROPS);
658
+ lines.push('/>');
659
+ return lines.join('\n');
660
+ }
661
+
662
+ // ─── Entry point ──────────────────────────────────────────────────────────────
663
+
664
+ module.exports = {
665
+ SDKSpecVersion: 'master',
666
+
667
+ customGenerator: function(mediaConfig, includeImports) {
668
+ var publicId = mediaConfig.public_id || '';
669
+ var actionsDTO = mediaConfig.actionsDTO || [];
670
+ var isVideo = (mediaConfig.resource_type || '').toLowerCase() === 'video';
671
+
672
+ var jsx, importStatement;
673
+
674
+ if (isVideo) {
675
+ var videoProps = mapActionsDTOToVideoProps(actionsDTO);
676
+ // CldVideoPlayer does not require width/height but they are strongly recommended
677
+ if (videoProps.width === undefined) videoProps.width = 1920;
678
+ if (videoProps.height === undefined) videoProps.height = 1080;
679
+ jsx = buildCldVideoPlayerJSX(publicId, videoProps);
680
+ importStatement = "import { CldVideoPlayer } from 'next-cloudinary';\n\n";
681
+ } else {
682
+ var imageProps = mapActionsDTOToImageProps(actionsDTO);
683
+ // Apply generative effects from parsed_url.transformation as a fallback.
684
+ // Ruby CGS ≤0.1.84 returns actions:null for e_gen_* so actionsDTO misses them.
685
+ applyGenerativeEffects(mediaConfig.transformation, imageProps);
686
+ // Overlay/underlay actions can't be reconstructed as named props — fall back
687
+ // to rawTransformations using the structured transformation array.
688
+ applyOverlayTransformations(mediaConfig.transformation, imageProps);
689
+ // next/image requires width and height — add defaults when not set by the URL
690
+ if (imageProps.width === undefined) imageProps.width = 960;
691
+ if (imageProps.height === undefined) imageProps.height = 600;
692
+ jsx = buildCldImageJSX(publicId, imageProps);
693
+ importStatement = "import { CldImage } from 'next-cloudinary';\n\n";
694
+ }
695
+
696
+ return includeImports ? importStatement + jsx : jsx;
697
+ },
698
+ };