@contentful/experiences-visual-editor-react 1.36.0 → 1.37.0-dev-20250423T1119-4e477a9.0

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.
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import styleInject from 'style-inject';
2
2
  import React, { useEffect, useRef, useState, useCallback, forwardRef, useLayoutEffect, useMemo } from 'react';
3
- import md5 from 'md5';
4
3
  import { z } from 'zod';
5
- import { BLOCKS } from '@contentful/rich-text-types';
6
4
  import { omit, isArray, isEqual, get as get$1 } from 'lodash-es';
5
+ import md5 from 'md5';
6
+ import { BLOCKS } from '@contentful/rich-text-types';
7
7
  import { create } from 'zustand';
8
8
  import { Droppable, Draggable, DragDropContext } from '@hello-pangea/dnd';
9
9
  import { produce } from 'immer';
@@ -163,497 +163,181 @@ const isStructureWithRelativeHeight = (componentId, height) => {
163
163
  return isContentfulStructureComponent(componentId) && !height?.toString().endsWith('px');
164
164
  };
165
165
 
166
- const findOutermostCoordinates = (first, second) => {
167
- return {
168
- top: Math.min(first.top, second.top),
169
- right: Math.max(first.right, second.right),
170
- bottom: Math.max(first.bottom, second.bottom),
171
- left: Math.min(first.left, second.left),
172
- };
173
- };
174
- const getElementCoordinates = (element) => {
175
- const rect = element.getBoundingClientRect();
176
- /**
177
- * If element does not have children, or element has it's own width or height,
178
- * return the element's coordinates.
179
- */
180
- if (element.children.length === 0 || rect.width !== 0 || rect.height !== 0) {
181
- return rect;
182
- }
183
- const rects = [];
184
- /**
185
- * If element has children, or element does not have it's own width and height,
186
- * we find the cordinates of the children, and assume the outermost coordinates of the children
187
- * as the coordinate of the element.
188
- *
189
- * E.g child1 => {top: 2, bottom: 3, left: 4, right: 6} & child2 => {top: 1, bottom: 8, left: 12, right: 24}
190
- * The final assumed coordinates of the element would be => { top: 1, right: 24, bottom: 8, left: 4 }
191
- */
192
- for (const child of element.children) {
193
- const childRect = getElementCoordinates(child);
194
- if (childRect.width !== 0 || childRect.height !== 0) {
195
- const { top, right, bottom, left } = childRect;
196
- rects.push({ top, right, bottom, left });
197
- }
198
- }
199
- if (rects.length === 0) {
200
- return rect;
201
- }
202
- const { top, right, bottom, left } = rects.reduce(findOutermostCoordinates);
203
- return DOMRect.fromRect({
204
- x: left,
205
- y: top,
206
- height: bottom - top,
207
- width: right - left,
208
- });
209
- };
210
-
211
- class ParseError extends Error {
212
- constructor(message) {
213
- super(message);
214
- }
215
- }
216
- const isValidJsonObject = (s) => {
217
- try {
218
- const result = JSON.parse(s);
219
- if ('object' !== typeof result) {
220
- return false;
221
- }
222
- return true;
223
- }
224
- catch (e) {
225
- return false;
226
- }
227
- };
228
- const doesMismatchMessageSchema = (event) => {
229
- try {
230
- tryParseMessage(event);
231
- return false;
232
- }
233
- catch (e) {
234
- if (e instanceof ParseError) {
235
- return e.message;
236
- }
237
- throw e;
238
- }
239
- };
240
- const tryParseMessage = (event) => {
241
- if (!event.data) {
242
- throw new ParseError('Field event.data is missing');
243
- }
244
- if ('string' !== typeof event.data) {
245
- throw new ParseError(`Field event.data must be a string, instead of '${typeof event.data}'`);
246
- }
247
- if (!isValidJsonObject(event.data)) {
248
- throw new ParseError('Field event.data must be a valid JSON object serialized as string');
249
- }
250
- const eventData = JSON.parse(event.data);
251
- if (!eventData.source) {
252
- throw new ParseError(`Field eventData.source must be equal to 'composability-app'`);
253
- }
254
- if ('composability-app' !== eventData.source) {
255
- throw new ParseError(`Field eventData.source must be equal to 'composability-app', instead of '${eventData.source}'`);
256
- }
257
- // check eventData.eventType
258
- const supportedEventTypes = Object.values(INCOMING_EVENTS$1);
259
- if (!supportedEventTypes.includes(eventData.eventType)) {
260
- // Expected message: This message is handled in the EntityStore to store fetched entities
261
- if (eventData.eventType !== PostMessageMethods$3.REQUESTED_ENTITIES) {
262
- throw new ParseError(`Field eventData.eventType must be one of the supported values: [${supportedEventTypes.join(', ')}]`);
263
- }
264
- }
265
- return eventData;
166
+ // These styles get added to every component, user custom or contentful provided
167
+ const builtInStyles = {
168
+ cfVerticalAlignment: {
169
+ validations: {
170
+ in: [
171
+ {
172
+ value: 'start',
173
+ displayName: 'Align left',
174
+ },
175
+ {
176
+ value: 'center',
177
+ displayName: 'Align center',
178
+ },
179
+ {
180
+ value: 'end',
181
+ displayName: 'Align right',
182
+ },
183
+ ],
184
+ },
185
+ type: 'Text',
186
+ group: 'style',
187
+ description: 'The vertical alignment of the section',
188
+ defaultValue: 'center',
189
+ displayName: 'Vertical alignment',
190
+ },
191
+ cfHorizontalAlignment: {
192
+ validations: {
193
+ in: [
194
+ {
195
+ value: 'start',
196
+ displayName: 'Align top',
197
+ },
198
+ {
199
+ value: 'center',
200
+ displayName: 'Align center',
201
+ },
202
+ {
203
+ value: 'end',
204
+ displayName: 'Align bottom',
205
+ },
206
+ ],
207
+ },
208
+ type: 'Text',
209
+ group: 'style',
210
+ description: 'The horizontal alignment of the section',
211
+ defaultValue: 'center',
212
+ displayName: 'Horizontal alignment',
213
+ },
214
+ cfVisibility: {
215
+ displayName: 'Visibility toggle',
216
+ type: 'Boolean',
217
+ group: 'style',
218
+ defaultValue: true,
219
+ description: 'The visibility of the component',
220
+ },
221
+ cfMargin: {
222
+ displayName: 'Margin',
223
+ type: 'Text',
224
+ group: 'style',
225
+ description: 'The margin of the section',
226
+ defaultValue: '0 0 0 0',
227
+ },
228
+ cfPadding: {
229
+ displayName: 'Padding',
230
+ type: 'Text',
231
+ group: 'style',
232
+ description: 'The padding of the section',
233
+ defaultValue: '0 0 0 0',
234
+ },
235
+ cfBackgroundColor: {
236
+ displayName: 'Background color',
237
+ type: 'Text',
238
+ group: 'style',
239
+ description: 'The background color of the section',
240
+ defaultValue: 'rgba(0, 0, 0, 0)',
241
+ },
242
+ cfWidth: {
243
+ displayName: 'Width',
244
+ type: 'Text',
245
+ group: 'style',
246
+ description: 'The width of the section',
247
+ defaultValue: '100%',
248
+ },
249
+ cfHeight: {
250
+ displayName: 'Height',
251
+ type: 'Text',
252
+ group: 'style',
253
+ description: 'The height of the section',
254
+ defaultValue: 'fit-content',
255
+ },
256
+ cfMaxWidth: {
257
+ displayName: 'Max width',
258
+ type: 'Text',
259
+ group: 'style',
260
+ description: 'The max-width of the section',
261
+ defaultValue: 'none',
262
+ },
263
+ cfFlexDirection: {
264
+ displayName: 'Direction',
265
+ type: 'Text',
266
+ group: 'style',
267
+ description: 'The orientation of the section',
268
+ defaultValue: 'column',
269
+ },
270
+ cfFlexReverse: {
271
+ displayName: 'Reverse Direction',
272
+ type: 'Boolean',
273
+ group: 'style',
274
+ description: 'Toggle the flex direction to be reversed',
275
+ defaultValue: false,
276
+ },
277
+ cfFlexWrap: {
278
+ displayName: 'Wrap objects',
279
+ type: 'Text',
280
+ group: 'style',
281
+ description: 'Wrap objects',
282
+ defaultValue: 'nowrap',
283
+ },
284
+ cfBorder: {
285
+ displayName: 'Border',
286
+ type: 'Text',
287
+ group: 'style',
288
+ description: 'The border of the section',
289
+ defaultValue: '0px solid rgba(0, 0, 0, 0)',
290
+ },
291
+ cfGap: {
292
+ displayName: 'Gap',
293
+ type: 'Text',
294
+ group: 'style',
295
+ description: 'The spacing between the elements of the section',
296
+ defaultValue: '0px',
297
+ },
298
+ cfHyperlink: {
299
+ displayName: 'URL',
300
+ type: 'Hyperlink',
301
+ defaultValue: '',
302
+ validations: {
303
+ format: 'URL',
304
+ bindingSourceType: ['entry', 'experience', 'manual'],
305
+ },
306
+ description: 'hyperlink for section or container',
307
+ },
308
+ cfOpenInNewTab: {
309
+ displayName: 'URL behaviour',
310
+ type: 'Boolean',
311
+ defaultValue: false,
312
+ description: 'Open in new tab',
313
+ },
266
314
  };
267
-
268
- const transformVisibility = (value) => {
269
- if (value === false) {
270
- return {
271
- display: 'none !important',
272
- };
273
- }
274
- // Don't explicitly set anything when visible to not overwrite values like `grid` or `flex`.
275
- return {};
276
- };
277
- // Keep this for backwards compatibility - deleting this would be a breaking change
278
- // because existing components on a users experience will have the width value as fill
279
- // rather than 100%
280
- const transformFill = (value) => (value === 'fill' ? '100%' : value);
281
- const transformGridColumn = (span) => {
282
- if (!span) {
283
- return {};
284
- }
285
- return {
286
- gridColumn: `span ${span}`,
287
- };
288
- };
289
- const transformBorderStyle = (value) => {
290
- if (!value)
291
- return {};
292
- const parts = value.split(' ');
293
- // Just accept the passed value
294
- if (parts.length < 3)
295
- return { border: value };
296
- const [borderSize, borderStyle, ...borderColorParts] = parts;
297
- const borderColor = borderColorParts.join(' ');
298
- return {
299
- border: `${borderSize} ${borderStyle} ${borderColor}`,
300
- };
301
- };
302
- const transformAlignment = (cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection = 'column') => cfFlexDirection === 'row'
303
- ? {
304
- alignItems: cfHorizontalAlignment,
305
- justifyContent: cfVerticalAlignment === 'center' ? `safe ${cfVerticalAlignment}` : cfVerticalAlignment,
306
- }
307
- : {
308
- alignItems: cfVerticalAlignment,
309
- justifyContent: cfHorizontalAlignment === 'center'
310
- ? `safe ${cfHorizontalAlignment}`
311
- : cfHorizontalAlignment,
312
- };
313
- const transformBackgroundImage = (cfBackgroundImageUrl, cfBackgroundImageOptions) => {
314
- const matchBackgroundSize = (scaling) => {
315
- if ('fill' === scaling)
316
- return 'cover';
317
- if ('fit' === scaling)
318
- return 'contain';
319
- };
320
- const matchBackgroundPosition = (alignment) => {
321
- if (!alignment || 'string' !== typeof alignment) {
322
- return;
323
- }
324
- let [horizontalAlignment, verticalAlignment] = alignment.trim().split(/\s+/, 2);
325
- // Special case for handling single values
326
- // for backwards compatibility with single values 'right','left', 'center', 'top','bottom'
327
- if (horizontalAlignment && !verticalAlignment) {
328
- const singleValue = horizontalAlignment;
329
- switch (singleValue) {
330
- case 'left':
331
- horizontalAlignment = 'left';
332
- verticalAlignment = 'center';
333
- break;
334
- case 'right':
335
- horizontalAlignment = 'right';
336
- verticalAlignment = 'center';
337
- break;
338
- case 'center':
339
- horizontalAlignment = 'center';
340
- verticalAlignment = 'center';
341
- break;
342
- case 'top':
343
- horizontalAlignment = 'center';
344
- verticalAlignment = 'top';
345
- break;
346
- case 'bottom':
347
- horizontalAlignment = 'center';
348
- verticalAlignment = 'bottom';
349
- break;
350
- // just fall down to the normal validation logic for horiz and vert
351
- }
352
- }
353
- const isHorizontalValid = ['left', 'right', 'center'].includes(horizontalAlignment);
354
- const isVerticalValid = ['top', 'bottom', 'center'].includes(verticalAlignment);
355
- horizontalAlignment = isHorizontalValid ? horizontalAlignment : 'left';
356
- verticalAlignment = isVerticalValid ? verticalAlignment : 'top';
357
- return `${horizontalAlignment} ${verticalAlignment}`;
358
- };
359
- if (!cfBackgroundImageUrl) {
360
- return;
361
- }
362
- let backgroundImage;
363
- let backgroundImageSet;
364
- if (typeof cfBackgroundImageUrl === 'string') {
365
- backgroundImage = `url(${cfBackgroundImageUrl})`;
366
- }
367
- else {
368
- const imgSet = cfBackgroundImageUrl.srcSet?.join(',');
369
- backgroundImage = `url(${cfBackgroundImageUrl.url})`;
370
- backgroundImageSet = `image-set(${imgSet})`;
371
- }
372
- return {
373
- backgroundImage,
374
- backgroundImage2: backgroundImageSet,
375
- backgroundRepeat: cfBackgroundImageOptions?.scaling === 'tile' ? 'repeat' : 'no-repeat',
376
- backgroundPosition: matchBackgroundPosition(cfBackgroundImageOptions?.alignment),
377
- backgroundSize: matchBackgroundSize(cfBackgroundImageOptions?.scaling),
378
- };
379
- };
380
-
381
- const toCSSAttribute = (key) => {
382
- let val = key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
383
- // Remove the number from the end of the key to allow for overrides on style properties
384
- val = val.replace(/\d+$/, '');
385
- return val;
386
- };
387
- /**
388
- * Turns a list of CSSProperties into a joined CSS string that can be
389
- * used for <style> tags. Per default it creates a minimized version.
390
- * For editor mode, use the `useWhitespaces` flag to create a more readable version.
391
- *
392
- * @param cssProperties list of CSS properties
393
- * @param useWhitespaces adds whitespaces and newlines between each rule
394
- * @returns a string of CSS rules
395
- */
396
- const stringifyCssProperties = (cssProperties, useWhitespaces = false) => {
397
- const rules = Object.entries(cssProperties)
398
- .filter(([, value]) => value !== undefined)
399
- .map(([key, value]) => useWhitespaces ? `${toCSSAttribute(key)}: ${value};` : `${toCSSAttribute(key)}:${value};`);
400
- return rules.join(useWhitespaces ? '\n' : '');
401
- };
402
- const buildStyleTag = ({ styles, nodeId }) => {
403
- const generatedStyles = stringifyCssProperties(styles, true);
404
- const className = `cfstyles-${nodeId ? nodeId : md5(generatedStyles)}`;
405
- const styleRule = `.${className}{ ${generatedStyles} }`;
406
- return [className, styleRule];
407
- };
408
- /**
409
- * Takes plain design values and transforms them into CSS properties. Undefined values will
410
- * be filtered out.
411
- *
412
- * **Example Input**
413
- * ```
414
- * values = {
415
- * cfVisibility: 'visible',
416
- * cfMargin: '10px',
417
- * cfFlexReverse: true,
418
- * cfImageOptions: { objectFit: 'cover' },
419
- * // ...
420
- * }
421
- * ```
422
- * **Example Output**
423
- * ```
424
- * cssProperties = {
425
- * margin: '10px',
426
- * flexDirection: 'row-reverse',
427
- * objectFit: 'cover',
428
- * // ...
429
- * }
430
- * ```
431
- */
432
- const buildCfStyles = (values) => {
433
- const cssProperties = {
434
- boxSizing: 'border-box',
435
- ...transformVisibility(values.cfVisibility),
436
- margin: values.cfMargin,
437
- padding: values.cfPadding,
438
- backgroundColor: values.cfBackgroundColor,
439
- width: transformFill(values.cfWidth || values.cfImageOptions?.width),
440
- height: transformFill(values.cfHeight || values.cfImageOptions?.height),
441
- maxWidth: values.cfMaxWidth,
442
- ...transformGridColumn(values.cfColumnSpan),
443
- ...transformBorderStyle(values.cfBorder),
444
- borderRadius: values.cfBorderRadius,
445
- gap: values.cfGap,
446
- ...transformAlignment(values.cfHorizontalAlignment, values.cfVerticalAlignment, values.cfFlexDirection),
447
- flexDirection: values.cfFlexReverse && values.cfFlexDirection
448
- ? `${values.cfFlexDirection}-reverse`
449
- : values.cfFlexDirection,
450
- flexWrap: values.cfFlexWrap,
451
- ...transformBackgroundImage(values.cfBackgroundImageUrl, values.cfBackgroundImageOptions),
452
- fontSize: values.cfFontSize,
453
- fontWeight: values.cfTextBold ? 'bold' : values.cfFontWeight,
454
- fontStyle: values.cfTextItalic ? 'italic' : undefined,
455
- textDecoration: values.cfTextUnderline ? 'underline' : undefined,
456
- lineHeight: values.cfLineHeight,
457
- letterSpacing: values.cfLetterSpacing,
458
- color: values.cfTextColor,
459
- textAlign: values.cfTextAlign,
460
- textTransform: values.cfTextTransform,
461
- objectFit: values.cfImageOptions?.objectFit,
462
- objectPosition: values.cfImageOptions?.objectPosition,
463
- };
464
- const cssPropertiesWithoutUndefined = Object.fromEntries(Object.entries(cssProperties).filter(([, value]) => value !== undefined));
465
- return cssPropertiesWithoutUndefined;
466
- };
467
- /**
468
- * Container/section default behavior:
469
- * Default height => height: EMPTY_CONTAINER_HEIGHT
470
- * If a container component has children => height: 'fit-content'
471
- */
472
- const calculateNodeDefaultHeight = ({ blockId, children, value, }) => {
473
- if (!blockId || !isContentfulStructureComponent(blockId) || value !== 'auto') {
474
- return value;
475
- }
476
- if (children.length) {
477
- return '100%';
478
- }
479
- return EMPTY_CONTAINER_HEIGHT$1;
480
- };
481
-
482
- // These styles get added to every component, user custom or contentful provided
483
- const builtInStyles = {
484
- cfVerticalAlignment: {
485
- validations: {
486
- in: [
487
- {
488
- value: 'start',
489
- displayName: 'Align left',
490
- },
491
- {
492
- value: 'center',
493
- displayName: 'Align center',
494
- },
495
- {
496
- value: 'end',
497
- displayName: 'Align right',
498
- },
499
- ],
500
- },
315
+ const optionalBuiltInStyles = {
316
+ cfFontSize: {
317
+ displayName: 'Font Size',
501
318
  type: 'Text',
502
319
  group: 'style',
503
- description: 'The vertical alignment of the section',
504
- defaultValue: 'center',
505
- displayName: 'Vertical alignment',
320
+ description: 'The font size of the element',
321
+ defaultValue: '16px',
506
322
  },
507
- cfHorizontalAlignment: {
323
+ cfFontWeight: {
508
324
  validations: {
509
325
  in: [
510
326
  {
511
- value: 'start',
512
- displayName: 'Align top',
327
+ value: '400',
328
+ displayName: 'Normal',
513
329
  },
514
330
  {
515
- value: 'center',
516
- displayName: 'Align center',
331
+ value: '500',
332
+ displayName: 'Medium',
517
333
  },
518
334
  {
519
- value: 'end',
520
- displayName: 'Align bottom',
335
+ value: '600',
336
+ displayName: 'Semi Bold',
521
337
  },
522
338
  ],
523
339
  },
524
- type: 'Text',
525
- group: 'style',
526
- description: 'The horizontal alignment of the section',
527
- defaultValue: 'center',
528
- displayName: 'Horizontal alignment',
529
- },
530
- cfVisibility: {
531
- displayName: 'Visibility toggle',
532
- type: 'Boolean',
533
- group: 'style',
534
- defaultValue: true,
535
- description: 'The visibility of the component',
536
- },
537
- cfMargin: {
538
- displayName: 'Margin',
539
- type: 'Text',
540
- group: 'style',
541
- description: 'The margin of the section',
542
- defaultValue: '0 0 0 0',
543
- },
544
- cfPadding: {
545
- displayName: 'Padding',
546
- type: 'Text',
547
- group: 'style',
548
- description: 'The padding of the section',
549
- defaultValue: '0 0 0 0',
550
- },
551
- cfBackgroundColor: {
552
- displayName: 'Background color',
553
- type: 'Text',
554
- group: 'style',
555
- description: 'The background color of the section',
556
- defaultValue: 'rgba(0, 0, 0, 0)',
557
- },
558
- cfWidth: {
559
- displayName: 'Width',
560
- type: 'Text',
561
- group: 'style',
562
- description: 'The width of the section',
563
- defaultValue: '100%',
564
- },
565
- cfHeight: {
566
- displayName: 'Height',
567
- type: 'Text',
568
- group: 'style',
569
- description: 'The height of the section',
570
- defaultValue: 'fit-content',
571
- },
572
- cfMaxWidth: {
573
- displayName: 'Max width',
574
- type: 'Text',
575
- group: 'style',
576
- description: 'The max-width of the section',
577
- defaultValue: 'none',
578
- },
579
- cfFlexDirection: {
580
- displayName: 'Direction',
581
- type: 'Text',
582
- group: 'style',
583
- description: 'The orientation of the section',
584
- defaultValue: 'column',
585
- },
586
- cfFlexReverse: {
587
- displayName: 'Reverse Direction',
588
- type: 'Boolean',
589
- group: 'style',
590
- description: 'Toggle the flex direction to be reversed',
591
- defaultValue: false,
592
- },
593
- cfFlexWrap: {
594
- displayName: 'Wrap objects',
595
- type: 'Text',
596
- group: 'style',
597
- description: 'Wrap objects',
598
- defaultValue: 'nowrap',
599
- },
600
- cfBorder: {
601
- displayName: 'Border',
602
- type: 'Text',
603
- group: 'style',
604
- description: 'The border of the section',
605
- defaultValue: '0px solid rgba(0, 0, 0, 0)',
606
- },
607
- cfGap: {
608
- displayName: 'Gap',
609
- type: 'Text',
610
- group: 'style',
611
- description: 'The spacing between the elements of the section',
612
- defaultValue: '0px',
613
- },
614
- cfHyperlink: {
615
- displayName: 'URL',
616
- type: 'Hyperlink',
617
- defaultValue: '',
618
- validations: {
619
- format: 'URL',
620
- bindingSourceType: ['entry', 'experience', 'manual'],
621
- },
622
- description: 'hyperlink for section or container',
623
- },
624
- cfOpenInNewTab: {
625
- displayName: 'URL behaviour',
626
- type: 'Boolean',
627
- defaultValue: false,
628
- description: 'Open in new tab',
629
- },
630
- };
631
- const optionalBuiltInStyles = {
632
- cfFontSize: {
633
- displayName: 'Font Size',
634
- type: 'Text',
635
- group: 'style',
636
- description: 'The font size of the element',
637
- defaultValue: '16px',
638
- },
639
- cfFontWeight: {
640
- validations: {
641
- in: [
642
- {
643
- value: '400',
644
- displayName: 'Normal',
645
- },
646
- {
647
- value: '500',
648
- displayName: 'Medium',
649
- },
650
- {
651
- value: '600',
652
- displayName: 'Semi Bold',
653
- },
654
- ],
655
- },
656
- displayName: 'Font Weight',
340
+ displayName: 'Font Weight',
657
341
  type: 'Text',
658
342
  group: 'style',
659
343
  description: 'The font weight of the element',
@@ -1271,725 +955,1041 @@ var CodeNames$1;
1271
955
  CodeNames["Custom"] = "custom";
1272
956
  })(CodeNames$1 || (CodeNames$1 = {}));
1273
957
 
1274
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1275
- function get(obj, path) {
1276
- if (!path.length) {
1277
- return obj;
1278
- }
1279
- try {
1280
- const [currentPath, ...nextPath] = path;
1281
- return get(obj[currentPath], nextPath);
1282
- }
1283
- catch (err) {
958
+ const MEDIA_QUERY_REGEXP = /(<|>)(\d{1,})(px|cm|mm|in|pt|pc)$/;
959
+ const toCSSMediaQuery = ({ query }) => {
960
+ if (query === '*')
1284
961
  return undefined;
962
+ const match = query.match(MEDIA_QUERY_REGEXP);
963
+ if (!match)
964
+ return undefined;
965
+ const [, operator, value, unit] = match;
966
+ if (operator === '<') {
967
+ const maxScreenWidth = Number(value) - 1;
968
+ return `(max-width: ${maxScreenWidth}${unit})`;
1285
969
  }
1286
- }
1287
-
1288
- const getBoundValue = (entryOrAsset, path) => {
1289
- const value = get(entryOrAsset, path.split('/').slice(2, -1));
1290
- return value && typeof value == 'object' && value.url
1291
- ? value.url
1292
- : value;
1293
- };
1294
-
1295
- const transformRichText = (entryOrAsset, entityStore, path) => {
1296
- const value = getBoundValue(entryOrAsset, path);
1297
- if (typeof value === 'string') {
1298
- return {
1299
- data: {},
1300
- content: [
1301
- {
1302
- nodeType: BLOCKS.PARAGRAPH,
1303
- data: {},
1304
- content: [
1305
- {
1306
- data: {},
1307
- nodeType: 'text',
1308
- value: value,
1309
- marks: [],
1310
- },
1311
- ],
1312
- },
1313
- ],
1314
- nodeType: BLOCKS.DOCUMENT,
1315
- };
1316
- }
1317
- if (typeof value === 'object' && value.nodeType === BLOCKS.DOCUMENT) {
1318
- // resolve any links to assets/entries/hyperlinks
1319
- const richTextDocument = value;
1320
- resolveLinks(richTextDocument, entityStore);
1321
- return richTextDocument;
970
+ else if (operator === '>') {
971
+ const minScreenWidth = Number(value) + 1;
972
+ return `(min-width: ${minScreenWidth}${unit})`;
1322
973
  }
1323
974
  return undefined;
1324
975
  };
1325
- const isLinkTarget = (node) => {
1326
- return node?.data?.target?.sys?.type === 'Link';
976
+ // Remove this helper when upgrading to TypeScript 5.0 - https://github.com/microsoft/TypeScript/issues/48829
977
+ const findLast = (array, predicate) => {
978
+ return array.reverse().find(predicate);
1327
979
  };
1328
- const resolveLinks = (node, entityStore) => {
1329
- if (!node)
1330
- return;
1331
- // Resolve link if current node has one
1332
- if (isLinkTarget(node)) {
1333
- const entity = entityStore.getEntityFromLink(node.data.target);
1334
- if (entity) {
1335
- node.data.target = entity;
1336
- }
1337
- }
1338
- // Process content array if it exists
1339
- if ('content' in node && Array.isArray(node.content)) {
1340
- node.content.forEach((childNode) => resolveLinks(childNode, entityStore));
1341
- }
980
+ // Initialise media query matchers. This won't include the always matching fallback breakpoint.
981
+ const mediaQueryMatcher = (breakpoints) => {
982
+ const mediaQueryMatches = {};
983
+ const mediaQueryMatchers = breakpoints
984
+ .map((breakpoint) => {
985
+ const cssMediaQuery = toCSSMediaQuery(breakpoint);
986
+ if (!cssMediaQuery)
987
+ return undefined;
988
+ if (typeof window === 'undefined')
989
+ return undefined;
990
+ const mediaQueryMatcher = window.matchMedia(cssMediaQuery);
991
+ mediaQueryMatches[breakpoint.id] = mediaQueryMatcher.matches;
992
+ return { id: breakpoint.id, signal: mediaQueryMatcher };
993
+ })
994
+ .filter((matcher) => !!matcher);
995
+ return [mediaQueryMatchers, mediaQueryMatches];
1342
996
  };
1343
-
1344
- function getOptimizedImageUrl(url, width, quality, format) {
1345
- if (url.startsWith('//')) {
1346
- url = 'https:' + url;
1347
- }
1348
- const params = new URLSearchParams();
1349
- if (width) {
1350
- params.append('w', width.toString());
1351
- }
1352
- if (quality && quality > 0 && quality < 100) {
1353
- params.append('q', quality.toString());
1354
- }
1355
- if (format) {
1356
- params.append('fm', format);
1357
- }
1358
- const queryString = params.toString();
1359
- return `${url}${queryString ? '?' + queryString : ''}`;
1360
- }
1361
-
1362
- function validateParams(file, quality, format) {
1363
- if (!file.details.image) {
1364
- throw Error('No image in file asset to transform');
1365
- }
1366
- if (quality < 0 || quality > 100) {
1367
- throw Error('Quality must be between 0 and 100');
1368
- }
1369
- if (format && !SUPPORTED_IMAGE_FORMATS.includes(format)) {
1370
- throw Error(`Format must be one of ${SUPPORTED_IMAGE_FORMATS.join(', ')}`);
1371
- }
1372
- return true;
1373
- }
1374
-
1375
- const MAX_WIDTH_ALLOWED$1 = 2000;
1376
- const getOptimizedBackgroundImageAsset = (file, widthStyle, quality = '100%', format) => {
1377
- const qualityNumber = Number(quality.replace('%', ''));
1378
- if (!validateParams(file, qualityNumber, format)) ;
1379
- if (!validateParams(file, qualityNumber, format)) ;
1380
- const url = file.url;
1381
- const { width1x, width2x } = getWidths(widthStyle, file);
1382
- const imageUrl1x = getOptimizedImageUrl(url, width1x, qualityNumber, format);
1383
- const imageUrl2x = getOptimizedImageUrl(url, width2x, qualityNumber, format);
1384
- const srcSet = [`url(${imageUrl1x}) 1x`, `url(${imageUrl2x}) 2x`];
1385
- const returnedUrl = getOptimizedImageUrl(url, width2x, qualityNumber, format);
1386
- const optimizedBackgroundImageAsset = {
1387
- url: returnedUrl,
1388
- srcSet,
1389
- file,
997
+ const getActiveBreakpointIndex = (breakpoints, mediaQueryMatches, fallbackBreakpointIndex) => {
998
+ // The breakpoints are ordered (desktop-first: descending by screen width)
999
+ const breakpointsWithMatches = breakpoints.map(({ id }, index) => ({
1000
+ id,
1001
+ index,
1002
+ // The fallback breakpoint with wildcard query will always match
1003
+ isMatch: mediaQueryMatches[id] ?? index === fallbackBreakpointIndex,
1004
+ }));
1005
+ // Find the last breakpoint in the list that matches (desktop-first: the narrowest one)
1006
+ const mostSpecificIndex = findLast(breakpointsWithMatches, ({ isMatch }) => isMatch)?.index;
1007
+ return mostSpecificIndex ?? fallbackBreakpointIndex;
1008
+ };
1009
+ const getFallbackBreakpointIndex = (breakpoints) => {
1010
+ // We assume that there will be a single breakpoint which uses the wildcard query.
1011
+ // If there is none, we just take the first one in the list.
1012
+ return Math.max(breakpoints.findIndex(({ query }) => query === '*'), 0);
1013
+ };
1014
+ const builtInStylesWithDesignTokens = [
1015
+ 'cfMargin',
1016
+ 'cfPadding',
1017
+ 'cfGap',
1018
+ 'cfWidth',
1019
+ 'cfHeight',
1020
+ 'cfBackgroundColor',
1021
+ 'cfBorder',
1022
+ 'cfBorderRadius',
1023
+ 'cfFontSize',
1024
+ 'cfLineHeight',
1025
+ 'cfLetterSpacing',
1026
+ 'cfTextColor',
1027
+ 'cfMaxWidth',
1028
+ ];
1029
+ const isValidBreakpointValue = (value) => {
1030
+ return value !== undefined && value !== null && value !== '';
1031
+ };
1032
+ const getValueForBreakpoint = (valuesByBreakpoint, breakpoints, activeBreakpointIndex, fallbackBreakpointIndex, variableName, resolveDesignTokens = true) => {
1033
+ const eventuallyResolveDesignTokens = (value) => {
1034
+ // For some built-in design properties, we support design tokens
1035
+ if (builtInStylesWithDesignTokens.includes(variableName)) {
1036
+ return getDesignTokenRegistration(value, variableName);
1037
+ }
1038
+ // For all other properties, we just return the breakpoint-specific value
1039
+ return value;
1390
1040
  };
1391
- return optimizedBackgroundImageAsset;
1392
- function getWidths(widthStyle, file) {
1393
- let width1x = 0;
1394
- let width2x = 0;
1395
- const intrinsicImageWidth = file.details.image.width;
1396
- if (widthStyle.endsWith('px')) {
1397
- width1x = Math.min(Number(widthStyle.replace('px', '')), intrinsicImageWidth);
1041
+ if (valuesByBreakpoint instanceof Object) {
1042
+ // Assume that the values are sorted by media query to apply the cascading CSS logic
1043
+ for (let index = activeBreakpointIndex; index >= 0; index--) {
1044
+ const breakpointId = breakpoints[index]?.id;
1045
+ if (isValidBreakpointValue(valuesByBreakpoint[breakpointId])) {
1046
+ // If the value is defined, we use it and stop the breakpoints cascade
1047
+ if (resolveDesignTokens) {
1048
+ return eventuallyResolveDesignTokens(valuesByBreakpoint[breakpointId]);
1049
+ }
1050
+ return valuesByBreakpoint[breakpointId];
1051
+ }
1398
1052
  }
1399
- else {
1400
- width1x = Math.min(MAX_WIDTH_ALLOWED$1, intrinsicImageWidth);
1053
+ const fallbackBreakpointId = breakpoints[fallbackBreakpointIndex]?.id;
1054
+ if (isValidBreakpointValue(valuesByBreakpoint[fallbackBreakpointId])) {
1055
+ if (resolveDesignTokens) {
1056
+ return eventuallyResolveDesignTokens(valuesByBreakpoint[fallbackBreakpointId]);
1057
+ }
1058
+ return valuesByBreakpoint[fallbackBreakpointId];
1401
1059
  }
1402
- width2x = Math.min(width1x * 2, intrinsicImageWidth);
1403
- return { width1x, width2x };
1404
1060
  }
1405
- };
1406
-
1407
- const MAX_WIDTH_ALLOWED = 4000;
1408
- const getOptimizedImageAsset = ({ file, sizes, loading, quality = '100%', format, }) => {
1409
- const qualityNumber = Number(quality.replace('%', ''));
1410
- if (!validateParams(file, qualityNumber, format)) ;
1411
- const url = file.url;
1412
- const maxWidth = Math.min(file.details.image.width, MAX_WIDTH_ALLOWED);
1413
- const numOfParts = Math.max(2, Math.ceil(maxWidth / 500));
1414
- const widthParts = Array.from({ length: numOfParts }, (_, index) => Math.ceil((index + 1) * (maxWidth / numOfParts)));
1415
- const srcSet = sizes
1416
- ? widthParts.map((width) => `${getOptimizedImageUrl(url, width, qualityNumber, format)} ${width}w`)
1417
- : [];
1418
- const intrinsicImageWidth = file.details.image.width;
1419
- if (intrinsicImageWidth > MAX_WIDTH_ALLOWED) {
1420
- srcSet.push(`${getOptimizedImageUrl(url, undefined, qualityNumber, format)} ${intrinsicImageWidth}w`);
1061
+ else {
1062
+ // Old design properties did not support breakpoints, keep for backward compatibility
1063
+ return valuesByBreakpoint;
1421
1064
  }
1422
- const returnedUrl = getOptimizedImageUrl(url, file.details.image.width > 2000 ? 2000 : undefined, qualityNumber, format);
1423
- const optimizedImageAsset = {
1424
- url: returnedUrl,
1425
- srcSet,
1426
- sizes,
1427
- file,
1428
- loading,
1429
- };
1430
- return optimizedImageAsset;
1431
1065
  };
1432
1066
 
1433
- const transformMedia = (asset, variables, resolveDesignValue, variableName, path) => {
1434
- let value;
1435
- // If it is not a deep path and not pointing to the file of the asset,
1436
- // it is just pointing to a normal field and therefore we just resolve the value as normal field
1437
- if (!isDeepPath(path) && !lastPathNamedSegmentEq(path, 'file')) {
1438
- return getBoundValue(asset, path);
1439
- }
1440
- //TODO: this will be better served by injectable type transformers instead of if statement
1441
- if (variableName === 'cfImageAsset') {
1442
- const optionsVariableName = 'cfImageOptions';
1443
- const options = resolveDesignValue(variables[optionsVariableName]?.type === 'DesignValue'
1444
- ? variables[optionsVariableName].valuesByBreakpoint
1445
- : {}, optionsVariableName);
1446
- if (!options) {
1447
- console.error(`Error transforming image asset: Required variable [${optionsVariableName}] missing from component definition`);
1067
+ const CF_DEBUG_KEY$1 = 'cf_debug';
1068
+ let DebugLogger$1 = class DebugLogger {
1069
+ constructor() {
1070
+ // Public methods for logging
1071
+ this.error = this.logger('error');
1072
+ this.warn = this.logger('warn');
1073
+ this.log = this.logger('log');
1074
+ this.debug = this.logger('debug');
1075
+ if (typeof localStorage === 'undefined') {
1076
+ this.enabled = false;
1448
1077
  return;
1449
1078
  }
1450
- try {
1451
- value = getOptimizedImageAsset({
1452
- file: asset.fields.file,
1453
- loading: options.loading,
1454
- sizes: options.targetSize,
1455
- quality: options.quality,
1456
- format: options.format,
1457
- });
1458
- return value;
1459
- }
1460
- catch (error) {
1461
- console.error('Error transforming image asset', error);
1079
+ // Default to checking localStorage for the debug mode on initialization if in browser
1080
+ this.enabled = localStorage.getItem(CF_DEBUG_KEY$1) === 'true';
1081
+ }
1082
+ static getInstance() {
1083
+ if (this.instance === null) {
1084
+ this.instance = new DebugLogger();
1462
1085
  }
1463
- return;
1086
+ return this.instance;
1464
1087
  }
1465
- if (variableName === 'cfBackgroundImageUrl') {
1466
- let width = resolveDesignValue(variables['cfWidth']?.type === 'DesignValue' ? variables['cfWidth'].valuesByBreakpoint : {}, 'cfWidth') || '100%';
1467
- const optionsVariableName = 'cfBackgroundImageOptions';
1468
- const options = resolveDesignValue(variables[optionsVariableName]?.type === 'DesignValue'
1469
- ? variables[optionsVariableName].valuesByBreakpoint
1470
- : {}, optionsVariableName);
1471
- if (!options) {
1472
- console.error(`Error transforming image asset: Required variable [${optionsVariableName}] missing from component definition`);
1088
+ getEnabled() {
1089
+ return this.enabled;
1090
+ }
1091
+ setEnabled(enabled) {
1092
+ this.enabled = enabled;
1093
+ if (typeof localStorage === 'undefined') {
1473
1094
  return;
1474
1095
  }
1475
- try {
1476
- // Target width (px/rem/em) will be applied to the css url if it's lower than the original image width (in px)
1477
- const assetDetails = asset.fields.file?.details;
1478
- const assetWidth = assetDetails?.image?.width || 0; // This is always in px
1479
- const targetWidthObject = parseCSSValue(options.targetSize); // Contains value and unit (px/rem/em) so convert and then compare to assetWidth
1480
- const targetValue = targetWidthObject
1481
- ? getTargetValueInPixels(targetWidthObject)
1482
- : assetWidth;
1483
- if (targetValue < assetWidth)
1484
- width = `${targetValue}px`;
1485
- value = getOptimizedBackgroundImageAsset(asset.fields.file, width, options.quality, options.format);
1486
- return value;
1096
+ if (enabled) {
1097
+ localStorage.setItem(CF_DEBUG_KEY$1, 'true');
1487
1098
  }
1488
- catch (error) {
1489
- console.error('Error transforming image asset', error);
1099
+ else {
1100
+ localStorage.removeItem(CF_DEBUG_KEY$1);
1490
1101
  }
1491
- return;
1492
1102
  }
1493
- return asset.fields.file?.url;
1103
+ // Log method for different levels (error, warn, log)
1104
+ logger(level) {
1105
+ return (...args) => {
1106
+ if (this.enabled) {
1107
+ console[level]('[cf-experiences-sdk]', ...args);
1108
+ }
1109
+ };
1110
+ }
1494
1111
  };
1112
+ DebugLogger$1.instance = null;
1113
+ DebugLogger$1.getInstance();
1495
1114
 
1496
- function getResolvedEntryFromLink(entryOrAsset, path, entityStore) {
1497
- if (entryOrAsset.sys.type === 'Asset') {
1498
- return entryOrAsset;
1115
+ const findOutermostCoordinates = (first, second) => {
1116
+ return {
1117
+ top: Math.min(first.top, second.top),
1118
+ right: Math.max(first.right, second.right),
1119
+ bottom: Math.max(first.bottom, second.bottom),
1120
+ left: Math.min(first.left, second.left),
1121
+ };
1122
+ };
1123
+ const getElementCoordinates = (element) => {
1124
+ const rect = element.getBoundingClientRect();
1125
+ /**
1126
+ * If element does not have children, or element has it's own width or height,
1127
+ * return the element's coordinates.
1128
+ */
1129
+ if (element.children.length === 0 || rect.width !== 0 || rect.height !== 0) {
1130
+ return rect;
1499
1131
  }
1500
- const value = get(entryOrAsset, path.split('/').slice(2, -1));
1501
- if (value?.sys.type !== 'Link') {
1502
- console.warn(`Expected a link to a reference, but got: ${JSON.stringify(value)}`);
1503
- return;
1132
+ const rects = [];
1133
+ /**
1134
+ * If element has children, or element does not have it's own width and height,
1135
+ * we find the cordinates of the children, and assume the outermost coordinates of the children
1136
+ * as the coordinate of the element.
1137
+ *
1138
+ * E.g child1 => {top: 2, bottom: 3, left: 4, right: 6} & child2 => {top: 1, bottom: 8, left: 12, right: 24}
1139
+ * The final assumed coordinates of the element would be => { top: 1, right: 24, bottom: 8, left: 4 }
1140
+ */
1141
+ for (const child of element.children) {
1142
+ const childRect = getElementCoordinates(child);
1143
+ if (childRect.width !== 0 || childRect.height !== 0) {
1144
+ const { top, right, bottom, left } = childRect;
1145
+ rects.push({ top, right, bottom, left });
1146
+ }
1504
1147
  }
1505
- //Look up the reference in the entity store
1506
- const resolvedEntity = entityStore.getEntityFromLink(value);
1507
- if (!resolvedEntity) {
1508
- return;
1148
+ if (rects.length === 0) {
1149
+ return rect;
1509
1150
  }
1510
- //resolve any embedded links - we currently only support 2 levels deep
1511
- const fields = resolvedEntity.fields || {};
1512
- Object.entries(fields).forEach(([fieldKey, field]) => {
1513
- if (field && field.sys?.type === 'Link') {
1514
- const entity = entityStore.getEntityFromLink(field);
1515
- if (entity) {
1516
- resolvedEntity.fields[fieldKey] = entity;
1517
- }
1518
- }
1519
- else if (field && Array.isArray(field)) {
1520
- resolvedEntity.fields[fieldKey] = field.map((innerField) => {
1521
- if (innerField && innerField.sys?.type === 'Link') {
1522
- const entity = entityStore.getEntityFromLink(innerField);
1523
- if (entity) {
1524
- return entity;
1525
- }
1526
- }
1527
- return innerField;
1528
- });
1529
- }
1151
+ const { top, right, bottom, left } = rects.reduce(findOutermostCoordinates);
1152
+ return DOMRect.fromRect({
1153
+ x: left,
1154
+ y: top,
1155
+ height: bottom - top,
1156
+ width: right - left,
1530
1157
  });
1531
- return resolvedEntity;
1158
+ };
1159
+
1160
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1161
+ const isLinkToAsset = (variable) => {
1162
+ if (!variable)
1163
+ return false;
1164
+ if (typeof variable !== 'object')
1165
+ return false;
1166
+ return (variable.sys?.linkType === 'Asset' &&
1167
+ typeof variable.sys?.id === 'string' &&
1168
+ !!variable.sys?.id &&
1169
+ variable.sys?.type === 'Link');
1170
+ };
1171
+
1172
+ const isLink = (maybeLink) => {
1173
+ if (maybeLink === null)
1174
+ return false;
1175
+ if (typeof maybeLink !== 'object')
1176
+ return false;
1177
+ const link = maybeLink;
1178
+ return Boolean(link.sys?.id) && link.sys?.type === 'Link';
1179
+ };
1180
+
1181
+ /**
1182
+ * This module encapsulates format of the path to a deep reference.
1183
+ */
1184
+ const parseDataSourcePathIntoFieldset = (path) => {
1185
+ const parsedPath = parseDeepPath(path);
1186
+ if (null === parsedPath) {
1187
+ throw new Error(`Cannot parse path '${path}' as deep path`);
1188
+ }
1189
+ return parsedPath.fields.map((field) => [null, field, '~locale']);
1190
+ };
1191
+ /**
1192
+ * Parse path into components, supports L1 references (one reference follow) atm.
1193
+ * @param path from data source. eg. `/uuid123/fields/image/~locale/fields/file/~locale`
1194
+ * eg. `/uuid123/fields/file/~locale/fields/title/~locale`
1195
+ * @returns
1196
+ */
1197
+ const parseDataSourcePathWithL1DeepBindings = (path) => {
1198
+ const parsedPath = parseDeepPath(path);
1199
+ if (null === parsedPath) {
1200
+ throw new Error(`Cannot parse path '${path}' as deep path`);
1201
+ }
1202
+ return {
1203
+ key: parsedPath.key,
1204
+ field: parsedPath.fields[0],
1205
+ referentField: parsedPath.fields[1],
1206
+ };
1207
+ };
1208
+ /**
1209
+ * Detects if paths is valid deep-path, like:
1210
+ * - /gV6yKXp61hfYrR7rEyKxY/fields/mainStory/~locale/fields/cover/~locale/fields/title/~locale
1211
+ * or regular, like:
1212
+ * - /6J8eA60yXwdm5eyUh9fX6/fields/mainStory/~locale
1213
+ * @returns
1214
+ */
1215
+ const isDeepPath = (deepPathCandidate) => {
1216
+ const deepPathParsed = parseDeepPath(deepPathCandidate);
1217
+ if (!deepPathParsed) {
1218
+ return false;
1219
+ }
1220
+ return deepPathParsed.fields.length > 1;
1221
+ };
1222
+ const parseDeepPath = (deepPathCandidate) => {
1223
+ // ALGORITHM:
1224
+ // We start with deep path in form:
1225
+ // /uuid123/fields/mainStory/~locale/fields/cover/~locale/fields/title/~locale
1226
+ // First turn string into array of segments
1227
+ // ['', 'uuid123', 'fields', 'mainStory', '~locale', 'fields', 'cover', '~locale', 'fields', 'title', '~locale']
1228
+ // Then group segments into intermediate represenatation - chunks, where each non-initial chunk starts with 'fields'
1229
+ // [
1230
+ // [ "", "uuid123" ],
1231
+ // [ "fields", "mainStory", "~locale" ],
1232
+ // [ "fields", "cover", "~locale" ],
1233
+ // [ "fields", "title", "~locale" ]
1234
+ // ]
1235
+ // Then check "initial" chunk for corretness
1236
+ // Then check all "field-leading" chunks for correctness
1237
+ const isValidInitialChunk = (initialChunk) => {
1238
+ // must have start with '' and have at least 2 segments, second non-empty
1239
+ // eg. /-_432uuid123123
1240
+ return /^\/([^/^~]+)$/.test(initialChunk.join('/'));
1241
+ };
1242
+ const isValidFieldChunk = (fieldChunk) => {
1243
+ // must start with 'fields' and have at least 3 segments, second non-empty and last segment must be '~locale'
1244
+ // eg. fields/-32234mainStory/~locale
1245
+ return /^fields\/[^/^~]+\/~locale$/.test(fieldChunk.join('/'));
1246
+ };
1247
+ const deepPathSegments = deepPathCandidate.split('/');
1248
+ const chunks = chunkSegments(deepPathSegments, { startNextChunkOnElementEqualTo: 'fields' });
1249
+ if (chunks.length <= 1) {
1250
+ return null; // malformed path, even regular paths have at least 2 chunks
1251
+ }
1252
+ else if (chunks.length === 2) {
1253
+ return null; // deep paths have at least 3 chunks
1254
+ }
1255
+ // With 3+ chunks we can now check for deep path correctness
1256
+ const [initialChunk, ...fieldChunks] = chunks;
1257
+ if (!isValidInitialChunk(initialChunk)) {
1258
+ return null;
1259
+ }
1260
+ if (!fieldChunks.every(isValidFieldChunk)) {
1261
+ return null;
1262
+ }
1263
+ return {
1264
+ key: initialChunk[1], // pick uuid from initial chunk ['','uuid123'],
1265
+ fields: fieldChunks.map((fieldChunk) => fieldChunk[1]), // pick only fieldName eg. from ['fields','mainStory', '~locale'] we pick `mainStory`
1266
+ };
1267
+ };
1268
+ const chunkSegments = (segments, { startNextChunkOnElementEqualTo }) => {
1269
+ const chunks = [];
1270
+ let currentChunk = [];
1271
+ const isSegmentBeginningOfChunk = (segment) => segment === startNextChunkOnElementEqualTo;
1272
+ const excludeEmptyChunks = (chunk) => chunk.length > 0;
1273
+ for (let i = 0; i < segments.length; i++) {
1274
+ const isInitialElement = i === 0;
1275
+ const segment = segments[i];
1276
+ if (isInitialElement) {
1277
+ currentChunk = [segment];
1278
+ }
1279
+ else if (isSegmentBeginningOfChunk(segment)) {
1280
+ chunks.push(currentChunk);
1281
+ currentChunk = [segment];
1282
+ }
1283
+ else {
1284
+ currentChunk.push(segment);
1285
+ }
1286
+ }
1287
+ chunks.push(currentChunk);
1288
+ return chunks.filter(excludeEmptyChunks);
1289
+ };
1290
+ const lastPathNamedSegmentEq = (path, expectedName) => {
1291
+ // `/key123/fields/featureImage/~locale/fields/file/~locale`
1292
+ // ['', 'key123', 'fields', 'featureImage', '~locale', 'fields', 'file', '~locale']
1293
+ const segments = path.split('/');
1294
+ if (segments.length < 2) {
1295
+ console.warn(`[experiences-sdk-react] Attempting to check whether last named segment of the path (${path}) equals to '${expectedName}', but the path doesn't have enough segments.`);
1296
+ return false;
1297
+ }
1298
+ const secondLast = segments[segments.length - 2]; // skipping trailing '~locale'
1299
+ return secondLast === expectedName;
1300
+ };
1301
+
1302
+ const resolveHyperlinkPattern = (pattern, entry, locale) => {
1303
+ if (!entry || !locale)
1304
+ return null;
1305
+ const variables = {
1306
+ entry,
1307
+ locale,
1308
+ };
1309
+ return buildTemplate({ template: pattern, context: variables });
1310
+ };
1311
+ function getValue(obj, path) {
1312
+ return path
1313
+ .replace(/\[/g, '.')
1314
+ .replace(/\]/g, '')
1315
+ .split('.')
1316
+ .reduce((o, k) => (o || {})[k], obj);
1317
+ }
1318
+ function addLocale(str, locale) {
1319
+ const fieldsIndicator = 'fields';
1320
+ const fieldsIndex = str.indexOf(fieldsIndicator);
1321
+ if (fieldsIndex !== -1) {
1322
+ const dotIndex = str.indexOf('.', fieldsIndex + fieldsIndicator.length + 1); // +1 for '.'
1323
+ if (dotIndex !== -1) {
1324
+ return str.slice(0, dotIndex + 1) + locale + '.' + str.slice(dotIndex + 1);
1325
+ }
1326
+ }
1327
+ return str;
1328
+ }
1329
+ function getTemplateValue(ctx, path) {
1330
+ const pathWithLocale = addLocale(path, ctx.locale);
1331
+ const retrievedValue = getValue(ctx, pathWithLocale);
1332
+ return typeof retrievedValue === 'object' && retrievedValue !== null
1333
+ ? retrievedValue[ctx.locale]
1334
+ : retrievedValue;
1335
+ }
1336
+ function buildTemplate({ template, context, }) {
1337
+ const localeVariable = /{\s*locale\s*}/g;
1338
+ // e.g. "{ page.sys.id }"
1339
+ const variables = /{\s*([\S]+?)\s*}/g;
1340
+ return (template
1341
+ // first replace the locale pattern
1342
+ .replace(localeVariable, context.locale)
1343
+ // then resolve the remaining variables
1344
+ .replace(variables, (_, path) => {
1345
+ const fallback = path + '_NOT_FOUND';
1346
+ const value = getTemplateValue(context, path) ?? fallback;
1347
+ // using _.result didn't gave proper results so we run our own version of it
1348
+ return String(typeof value === 'function' ? value() : value);
1349
+ }));
1350
+ }
1351
+
1352
+ const stylesToKeep = ['cfImageAsset'];
1353
+ const stylesToRemove = CF_STYLE_ATTRIBUTES.filter((style) => !stylesToKeep.includes(style));
1354
+ const propsToRemove = ['cfHyperlink', 'cfOpenInNewTab', 'cfSsrClassName'];
1355
+ const sanitizeNodeProps = (nodeProps) => {
1356
+ return omit(nodeProps, stylesToRemove, propsToRemove);
1357
+ };
1358
+
1359
+ const transformVisibility = (value) => {
1360
+ if (value === false) {
1361
+ return {
1362
+ display: 'none !important',
1363
+ };
1364
+ }
1365
+ // Don't explicitly set anything when visible to not overwrite values like `grid` or `flex`.
1366
+ return {};
1367
+ };
1368
+ // Keep this for backwards compatibility - deleting this would be a breaking change
1369
+ // because existing components on a users experience will have the width value as fill
1370
+ // rather than 100%
1371
+ const transformFill = (value) => (value === 'fill' ? '100%' : value);
1372
+ const transformGridColumn = (span) => {
1373
+ if (!span) {
1374
+ return {};
1375
+ }
1376
+ return {
1377
+ gridColumn: `span ${span}`,
1378
+ };
1379
+ };
1380
+ const transformBorderStyle = (value) => {
1381
+ if (!value)
1382
+ return {};
1383
+ const parts = value.split(' ');
1384
+ // Just accept the passed value
1385
+ if (parts.length < 3)
1386
+ return { border: value };
1387
+ const [borderSize, borderStyle, ...borderColorParts] = parts;
1388
+ const borderColor = borderColorParts.join(' ');
1389
+ return {
1390
+ border: `${borderSize} ${borderStyle} ${borderColor}`,
1391
+ };
1392
+ };
1393
+ const transformAlignment = (cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection = 'column') => cfFlexDirection === 'row'
1394
+ ? {
1395
+ alignItems: cfHorizontalAlignment,
1396
+ justifyContent: cfVerticalAlignment === 'center' ? `safe ${cfVerticalAlignment}` : cfVerticalAlignment,
1397
+ }
1398
+ : {
1399
+ alignItems: cfVerticalAlignment,
1400
+ justifyContent: cfHorizontalAlignment === 'center'
1401
+ ? `safe ${cfHorizontalAlignment}`
1402
+ : cfHorizontalAlignment,
1403
+ };
1404
+ const transformBackgroundImage = (cfBackgroundImageUrl, cfBackgroundImageOptions) => {
1405
+ const matchBackgroundSize = (scaling) => {
1406
+ if ('fill' === scaling)
1407
+ return 'cover';
1408
+ if ('fit' === scaling)
1409
+ return 'contain';
1410
+ };
1411
+ const matchBackgroundPosition = (alignment) => {
1412
+ if (!alignment || 'string' !== typeof alignment) {
1413
+ return;
1414
+ }
1415
+ let [horizontalAlignment, verticalAlignment] = alignment.trim().split(/\s+/, 2);
1416
+ // Special case for handling single values
1417
+ // for backwards compatibility with single values 'right','left', 'center', 'top','bottom'
1418
+ if (horizontalAlignment && !verticalAlignment) {
1419
+ const singleValue = horizontalAlignment;
1420
+ switch (singleValue) {
1421
+ case 'left':
1422
+ horizontalAlignment = 'left';
1423
+ verticalAlignment = 'center';
1424
+ break;
1425
+ case 'right':
1426
+ horizontalAlignment = 'right';
1427
+ verticalAlignment = 'center';
1428
+ break;
1429
+ case 'center':
1430
+ horizontalAlignment = 'center';
1431
+ verticalAlignment = 'center';
1432
+ break;
1433
+ case 'top':
1434
+ horizontalAlignment = 'center';
1435
+ verticalAlignment = 'top';
1436
+ break;
1437
+ case 'bottom':
1438
+ horizontalAlignment = 'center';
1439
+ verticalAlignment = 'bottom';
1440
+ break;
1441
+ // just fall down to the normal validation logic for horiz and vert
1442
+ }
1443
+ }
1444
+ const isHorizontalValid = ['left', 'right', 'center'].includes(horizontalAlignment);
1445
+ const isVerticalValid = ['top', 'bottom', 'center'].includes(verticalAlignment);
1446
+ horizontalAlignment = isHorizontalValid ? horizontalAlignment : 'left';
1447
+ verticalAlignment = isVerticalValid ? verticalAlignment : 'top';
1448
+ return `${horizontalAlignment} ${verticalAlignment}`;
1449
+ };
1450
+ if (!cfBackgroundImageUrl) {
1451
+ return;
1452
+ }
1453
+ let backgroundImage;
1454
+ let backgroundImageSet;
1455
+ if (typeof cfBackgroundImageUrl === 'string') {
1456
+ backgroundImage = `url(${cfBackgroundImageUrl})`;
1457
+ }
1458
+ else {
1459
+ const imgSet = cfBackgroundImageUrl.srcSet?.join(',');
1460
+ backgroundImage = `url(${cfBackgroundImageUrl.url})`;
1461
+ backgroundImageSet = `image-set(${imgSet})`;
1462
+ }
1463
+ return {
1464
+ backgroundImage,
1465
+ backgroundImage2: backgroundImageSet,
1466
+ backgroundRepeat: cfBackgroundImageOptions?.scaling === 'tile' ? 'repeat' : 'no-repeat',
1467
+ backgroundPosition: matchBackgroundPosition(cfBackgroundImageOptions?.alignment),
1468
+ backgroundSize: matchBackgroundSize(cfBackgroundImageOptions?.scaling),
1469
+ };
1470
+ };
1471
+
1472
+ const toCSSAttribute = (key) => {
1473
+ let val = key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
1474
+ // Remove the number from the end of the key to allow for overrides on style properties
1475
+ val = val.replace(/\d+$/, '');
1476
+ return val;
1477
+ };
1478
+ /**
1479
+ * Turns a list of CSSProperties into a joined CSS string that can be
1480
+ * used for <style> tags. Per default it creates a minimized version.
1481
+ * For editor mode, use the `useWhitespaces` flag to create a more readable version.
1482
+ *
1483
+ * @param cssProperties list of CSS properties
1484
+ * @param useWhitespaces adds whitespaces and newlines between each rule
1485
+ * @returns a string of CSS rules
1486
+ */
1487
+ const stringifyCssProperties = (cssProperties, useWhitespaces = false) => {
1488
+ const rules = Object.entries(cssProperties)
1489
+ .filter(([, value]) => value !== undefined)
1490
+ .map(([key, value]) => useWhitespaces ? `${toCSSAttribute(key)}: ${value};` : `${toCSSAttribute(key)}:${value};`);
1491
+ return rules.join(useWhitespaces ? '\n' : '');
1492
+ };
1493
+ const buildStyleTag = ({ styles, nodeId }) => {
1494
+ const generatedStyles = stringifyCssProperties(styles, true);
1495
+ const className = `cfstyles-${nodeId ? nodeId : md5(generatedStyles)}`;
1496
+ const styleRule = `.${className}{ ${generatedStyles} }`;
1497
+ return [className, styleRule];
1498
+ };
1499
+ /**
1500
+ * Takes plain design values and transforms them into CSS properties. Undefined values will
1501
+ * be filtered out.
1502
+ *
1503
+ * **Example Input**
1504
+ * ```
1505
+ * values = {
1506
+ * cfVisibility: 'visible',
1507
+ * cfMargin: '10px',
1508
+ * cfFlexReverse: true,
1509
+ * cfImageOptions: { objectFit: 'cover' },
1510
+ * // ...
1511
+ * }
1512
+ * ```
1513
+ * **Example Output**
1514
+ * ```
1515
+ * cssProperties = {
1516
+ * margin: '10px',
1517
+ * flexDirection: 'row-reverse',
1518
+ * objectFit: 'cover',
1519
+ * // ...
1520
+ * }
1521
+ * ```
1522
+ */
1523
+ const buildCfStyles = (values) => {
1524
+ const cssProperties = {
1525
+ boxSizing: 'border-box',
1526
+ ...transformVisibility(values.cfVisibility),
1527
+ margin: values.cfMargin,
1528
+ padding: values.cfPadding,
1529
+ backgroundColor: values.cfBackgroundColor,
1530
+ width: transformFill(values.cfWidth || values.cfImageOptions?.width),
1531
+ height: transformFill(values.cfHeight || values.cfImageOptions?.height),
1532
+ maxWidth: values.cfMaxWidth,
1533
+ ...transformGridColumn(values.cfColumnSpan),
1534
+ ...transformBorderStyle(values.cfBorder),
1535
+ borderRadius: values.cfBorderRadius,
1536
+ gap: values.cfGap,
1537
+ ...transformAlignment(values.cfHorizontalAlignment, values.cfVerticalAlignment, values.cfFlexDirection),
1538
+ flexDirection: values.cfFlexReverse && values.cfFlexDirection
1539
+ ? `${values.cfFlexDirection}-reverse`
1540
+ : values.cfFlexDirection,
1541
+ flexWrap: values.cfFlexWrap,
1542
+ ...transformBackgroundImage(values.cfBackgroundImageUrl, values.cfBackgroundImageOptions),
1543
+ fontSize: values.cfFontSize,
1544
+ fontWeight: values.cfTextBold ? 'bold' : values.cfFontWeight,
1545
+ fontStyle: values.cfTextItalic ? 'italic' : undefined,
1546
+ textDecoration: values.cfTextUnderline ? 'underline' : undefined,
1547
+ lineHeight: values.cfLineHeight,
1548
+ letterSpacing: values.cfLetterSpacing,
1549
+ color: values.cfTextColor,
1550
+ textAlign: values.cfTextAlign,
1551
+ textTransform: values.cfTextTransform,
1552
+ objectFit: values.cfImageOptions?.objectFit,
1553
+ objectPosition: values.cfImageOptions?.objectPosition,
1554
+ };
1555
+ const cssPropertiesWithoutUndefined = Object.fromEntries(Object.entries(cssProperties).filter(([, value]) => value !== undefined));
1556
+ return cssPropertiesWithoutUndefined;
1557
+ };
1558
+ /**
1559
+ * Container/section default behavior:
1560
+ * Default height => height: EMPTY_CONTAINER_HEIGHT
1561
+ * If a container component has children => height: 'fit-content'
1562
+ */
1563
+ const calculateNodeDefaultHeight = ({ blockId, children, value, }) => {
1564
+ if (!blockId || !isContentfulStructureComponent(blockId) || value !== 'auto') {
1565
+ return value;
1566
+ }
1567
+ if (children.length) {
1568
+ return '100%';
1569
+ }
1570
+ return EMPTY_CONTAINER_HEIGHT$1;
1571
+ };
1572
+
1573
+ function getOptimizedImageUrl(url, width, quality, format) {
1574
+ if (url.startsWith('//')) {
1575
+ url = 'https:' + url;
1576
+ }
1577
+ const params = new URLSearchParams();
1578
+ if (width) {
1579
+ params.append('w', width.toString());
1580
+ }
1581
+ if (quality && quality > 0 && quality < 100) {
1582
+ params.append('q', quality.toString());
1583
+ }
1584
+ if (format) {
1585
+ params.append('fm', format);
1586
+ }
1587
+ const queryString = params.toString();
1588
+ return `${url}${queryString ? '?' + queryString : ''}`;
1532
1589
  }
1533
1590
 
1534
- function getArrayValue(entryOrAsset, path, entityStore) {
1535
- if (entryOrAsset.sys.type === 'Asset') {
1536
- return entryOrAsset;
1591
+ function validateParams(file, quality, format) {
1592
+ if (!file.details.image) {
1593
+ throw Error('No image in file asset to transform');
1537
1594
  }
1538
- const arrayValue = get(entryOrAsset, path.split('/').slice(2, -1));
1539
- if (!isArray(arrayValue)) {
1540
- console.warn(`Expected a value to be an array, but got: ${JSON.stringify(arrayValue)}`);
1541
- return;
1595
+ if (quality < 0 || quality > 100) {
1596
+ throw Error('Quality must be between 0 and 100');
1542
1597
  }
1543
- const result = arrayValue.map((value) => {
1544
- if (typeof value === 'string') {
1545
- return value;
1546
- }
1547
- else if (value?.sys?.type === 'Link') {
1548
- const resolvedEntity = entityStore.getEntityFromLink(value);
1549
- if (!resolvedEntity) {
1550
- return;
1551
- }
1552
- //resolve any embedded links - we currently only support 2 levels deep
1553
- const fields = resolvedEntity.fields || {};
1554
- Object.entries(fields).forEach(([fieldKey, field]) => {
1555
- if (field && field.sys?.type === 'Link') {
1556
- const entity = entityStore.getEntityFromLink(field);
1557
- if (entity) {
1558
- resolvedEntity.fields[fieldKey] = entity;
1559
- }
1560
- }
1561
- });
1562
- return resolvedEntity;
1598
+ if (format && !SUPPORTED_IMAGE_FORMATS.includes(format)) {
1599
+ throw Error(`Format must be one of ${SUPPORTED_IMAGE_FORMATS.join(', ')}`);
1600
+ }
1601
+ return true;
1602
+ }
1603
+
1604
+ const MAX_WIDTH_ALLOWED$1 = 2000;
1605
+ const getOptimizedBackgroundImageAsset = (file, widthStyle, quality = '100%', format) => {
1606
+ const qualityNumber = Number(quality.replace('%', ''));
1607
+ if (!validateParams(file, qualityNumber, format)) ;
1608
+ if (!validateParams(file, qualityNumber, format)) ;
1609
+ const url = file.url;
1610
+ const { width1x, width2x } = getWidths(widthStyle, file);
1611
+ const imageUrl1x = getOptimizedImageUrl(url, width1x, qualityNumber, format);
1612
+ const imageUrl2x = getOptimizedImageUrl(url, width2x, qualityNumber, format);
1613
+ const srcSet = [`url(${imageUrl1x}) 1x`, `url(${imageUrl2x}) 2x`];
1614
+ const returnedUrl = getOptimizedImageUrl(url, width2x, qualityNumber, format);
1615
+ const optimizedBackgroundImageAsset = {
1616
+ url: returnedUrl,
1617
+ srcSet,
1618
+ file,
1619
+ };
1620
+ return optimizedBackgroundImageAsset;
1621
+ function getWidths(widthStyle, file) {
1622
+ let width1x = 0;
1623
+ let width2x = 0;
1624
+ const intrinsicImageWidth = file.details.image.width;
1625
+ if (widthStyle.endsWith('px')) {
1626
+ width1x = Math.min(Number(widthStyle.replace('px', '')), intrinsicImageWidth);
1563
1627
  }
1564
1628
  else {
1565
- console.warn(`Expected value to be a string or Link, but got: ${JSON.stringify(value)}`);
1566
- return undefined;
1629
+ width1x = Math.min(MAX_WIDTH_ALLOWED$1, intrinsicImageWidth);
1567
1630
  }
1568
- });
1569
- return result;
1570
- }
1571
-
1572
- const transformBoundContentValue = (variables, entityStore, binding, resolveDesignValue, variableName, variableType, path) => {
1573
- const entityOrAsset = entityStore.getEntryOrAsset(binding, path);
1574
- if (!entityOrAsset)
1575
- return;
1576
- switch (variableType) {
1577
- case 'Media':
1578
- // If we bound a normal entry field to the media variable we just return the bound value
1579
- if (entityOrAsset.sys.type === 'Entry') {
1580
- return getBoundValue(entityOrAsset, path);
1581
- }
1582
- return transformMedia(entityOrAsset, variables, resolveDesignValue, variableName, path);
1583
- case 'RichText':
1584
- return transformRichText(entityOrAsset, entityStore, path);
1585
- case 'Array':
1586
- return getArrayValue(entityOrAsset, path, entityStore);
1587
- case 'Link':
1588
- return getResolvedEntryFromLink(entityOrAsset, path, entityStore);
1589
- default:
1590
- return getBoundValue(entityOrAsset, path);
1631
+ width2x = Math.min(width1x * 2, intrinsicImageWidth);
1632
+ return { width1x, width2x };
1591
1633
  }
1592
1634
  };
1593
1635
 
1594
- const getDataFromTree = (tree) => {
1595
- let dataSource = {};
1596
- let unboundValues = {};
1597
- const queue = [...tree.root.children];
1598
- while (queue.length) {
1599
- const node = queue.shift();
1600
- if (!node) {
1601
- continue;
1602
- }
1603
- dataSource = { ...dataSource, ...node.data.dataSource };
1604
- unboundValues = { ...unboundValues, ...node.data.unboundValues };
1605
- if (node.children.length) {
1606
- queue.push(...node.children);
1607
- }
1636
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1637
+ function get(obj, path) {
1638
+ if (!path.length) {
1639
+ return obj;
1608
1640
  }
1609
- return {
1610
- dataSource,
1611
- unboundValues,
1612
- };
1613
- };
1614
- function parseCSSValue(input) {
1615
- const regex = /^(\d+(\.\d+)?)(px|em|rem)$/;
1616
- const match = input.match(regex);
1617
- if (match) {
1618
- return {
1619
- value: parseFloat(match[1]),
1620
- unit: match[3],
1621
- };
1641
+ try {
1642
+ const [currentPath, ...nextPath] = path;
1643
+ return get(obj[currentPath], nextPath);
1622
1644
  }
1623
- return null;
1624
- }
1625
- function getTargetValueInPixels(targetWidthObject) {
1626
- switch (targetWidthObject.unit) {
1627
- case 'px':
1628
- return targetWidthObject.value;
1629
- case 'em':
1630
- return targetWidthObject.value * 16;
1631
- case 'rem':
1632
- return targetWidthObject.value * 16;
1633
- default:
1634
- return targetWidthObject.value;
1645
+ catch (err) {
1646
+ return undefined;
1635
1647
  }
1636
1648
  }
1637
1649
 
1638
- const MEDIA_QUERY_REGEXP = /(<|>)(\d{1,})(px|cm|mm|in|pt|pc)$/;
1639
- const toCSSMediaQuery = ({ query }) => {
1640
- if (query === '*')
1641
- return undefined;
1642
- const match = query.match(MEDIA_QUERY_REGEXP);
1643
- if (!match)
1644
- return undefined;
1645
- const [, operator, value, unit] = match;
1646
- if (operator === '<') {
1647
- const maxScreenWidth = Number(value) - 1;
1648
- return `(max-width: ${maxScreenWidth}${unit})`;
1650
+ const getBoundValue = (entryOrAsset, path) => {
1651
+ const value = get(entryOrAsset, path.split('/').slice(2, -1));
1652
+ return value && typeof value == 'object' && value.url
1653
+ ? value.url
1654
+ : value;
1655
+ };
1656
+
1657
+ const transformRichText = (entryOrAsset, entityStore, path) => {
1658
+ const value = getBoundValue(entryOrAsset, path);
1659
+ if (typeof value === 'string') {
1660
+ return {
1661
+ data: {},
1662
+ content: [
1663
+ {
1664
+ nodeType: BLOCKS.PARAGRAPH,
1665
+ data: {},
1666
+ content: [
1667
+ {
1668
+ data: {},
1669
+ nodeType: 'text',
1670
+ value: value,
1671
+ marks: [],
1672
+ },
1673
+ ],
1674
+ },
1675
+ ],
1676
+ nodeType: BLOCKS.DOCUMENT,
1677
+ };
1649
1678
  }
1650
- else if (operator === '>') {
1651
- const minScreenWidth = Number(value) + 1;
1652
- return `(min-width: ${minScreenWidth}${unit})`;
1679
+ if (typeof value === 'object' && value.nodeType === BLOCKS.DOCUMENT) {
1680
+ // resolve any links to assets/entries/hyperlinks
1681
+ const richTextDocument = value;
1682
+ resolveLinks(richTextDocument, entityStore);
1683
+ return richTextDocument;
1653
1684
  }
1654
1685
  return undefined;
1655
1686
  };
1656
- // Remove this helper when upgrading to TypeScript 5.0 - https://github.com/microsoft/TypeScript/issues/48829
1657
- const findLast = (array, predicate) => {
1658
- return array.reverse().find(predicate);
1659
- };
1660
- // Initialise media query matchers. This won't include the always matching fallback breakpoint.
1661
- const mediaQueryMatcher = (breakpoints) => {
1662
- const mediaQueryMatches = {};
1663
- const mediaQueryMatchers = breakpoints
1664
- .map((breakpoint) => {
1665
- const cssMediaQuery = toCSSMediaQuery(breakpoint);
1666
- if (!cssMediaQuery)
1667
- return undefined;
1668
- if (typeof window === 'undefined')
1669
- return undefined;
1670
- const mediaQueryMatcher = window.matchMedia(cssMediaQuery);
1671
- mediaQueryMatches[breakpoint.id] = mediaQueryMatcher.matches;
1672
- return { id: breakpoint.id, signal: mediaQueryMatcher };
1673
- })
1674
- .filter((matcher) => !!matcher);
1675
- return [mediaQueryMatchers, mediaQueryMatches];
1676
- };
1677
- const getActiveBreakpointIndex = (breakpoints, mediaQueryMatches, fallbackBreakpointIndex) => {
1678
- // The breakpoints are ordered (desktop-first: descending by screen width)
1679
- const breakpointsWithMatches = breakpoints.map(({ id }, index) => ({
1680
- id,
1681
- index,
1682
- // The fallback breakpoint with wildcard query will always match
1683
- isMatch: mediaQueryMatches[id] ?? index === fallbackBreakpointIndex,
1684
- }));
1685
- // Find the last breakpoint in the list that matches (desktop-first: the narrowest one)
1686
- const mostSpecificIndex = findLast(breakpointsWithMatches, ({ isMatch }) => isMatch)?.index;
1687
- return mostSpecificIndex ?? fallbackBreakpointIndex;
1688
- };
1689
- const getFallbackBreakpointIndex = (breakpoints) => {
1690
- // We assume that there will be a single breakpoint which uses the wildcard query.
1691
- // If there is none, we just take the first one in the list.
1692
- return Math.max(breakpoints.findIndex(({ query }) => query === '*'), 0);
1693
- };
1694
- const builtInStylesWithDesignTokens = [
1695
- 'cfMargin',
1696
- 'cfPadding',
1697
- 'cfGap',
1698
- 'cfWidth',
1699
- 'cfHeight',
1700
- 'cfBackgroundColor',
1701
- 'cfBorder',
1702
- 'cfBorderRadius',
1703
- 'cfFontSize',
1704
- 'cfLineHeight',
1705
- 'cfLetterSpacing',
1706
- 'cfTextColor',
1707
- 'cfMaxWidth',
1708
- ];
1709
- const isValidBreakpointValue = (value) => {
1710
- return value !== undefined && value !== null && value !== '';
1687
+ const isLinkTarget = (node) => {
1688
+ return node?.data?.target?.sys?.type === 'Link';
1711
1689
  };
1712
- const getValueForBreakpoint = (valuesByBreakpoint, breakpoints, activeBreakpointIndex, fallbackBreakpointIndex, variableName, resolveDesignTokens = true) => {
1713
- const eventuallyResolveDesignTokens = (value) => {
1714
- // For some built-in design properties, we support design tokens
1715
- if (builtInStylesWithDesignTokens.includes(variableName)) {
1716
- return getDesignTokenRegistration(value, variableName);
1717
- }
1718
- // For all other properties, we just return the breakpoint-specific value
1719
- return value;
1720
- };
1721
- if (valuesByBreakpoint instanceof Object) {
1722
- // Assume that the values are sorted by media query to apply the cascading CSS logic
1723
- for (let index = activeBreakpointIndex; index >= 0; index--) {
1724
- const breakpointId = breakpoints[index]?.id;
1725
- if (isValidBreakpointValue(valuesByBreakpoint[breakpointId])) {
1726
- // If the value is defined, we use it and stop the breakpoints cascade
1727
- if (resolveDesignTokens) {
1728
- return eventuallyResolveDesignTokens(valuesByBreakpoint[breakpointId]);
1729
- }
1730
- return valuesByBreakpoint[breakpointId];
1731
- }
1732
- }
1733
- const fallbackBreakpointId = breakpoints[fallbackBreakpointIndex]?.id;
1734
- if (isValidBreakpointValue(valuesByBreakpoint[fallbackBreakpointId])) {
1735
- if (resolveDesignTokens) {
1736
- return eventuallyResolveDesignTokens(valuesByBreakpoint[fallbackBreakpointId]);
1737
- }
1738
- return valuesByBreakpoint[fallbackBreakpointId];
1690
+ const resolveLinks = (node, entityStore) => {
1691
+ if (!node)
1692
+ return;
1693
+ // Resolve link if current node has one
1694
+ if (isLinkTarget(node)) {
1695
+ const entity = entityStore.getEntityFromLink(node.data.target);
1696
+ if (entity) {
1697
+ node.data.target = entity;
1739
1698
  }
1740
1699
  }
1741
- else {
1742
- // Old design properties did not support breakpoints, keep for backward compatibility
1743
- return valuesByBreakpoint;
1700
+ // Process content array if it exists
1701
+ if ('content' in node && Array.isArray(node.content)) {
1702
+ node.content.forEach((childNode) => resolveLinks(childNode, entityStore));
1744
1703
  }
1745
1704
  };
1746
1705
 
1747
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1748
- const isLinkToAsset = (variable) => {
1749
- if (!variable)
1750
- return false;
1751
- if (typeof variable !== 'object')
1752
- return false;
1753
- return (variable.sys?.linkType === 'Asset' &&
1754
- typeof variable.sys?.id === 'string' &&
1755
- !!variable.sys?.id &&
1756
- variable.sys?.type === 'Link');
1757
- };
1758
-
1759
- const isLink = (maybeLink) => {
1760
- if (maybeLink === null)
1761
- return false;
1762
- if (typeof maybeLink !== 'object')
1763
- return false;
1764
- const link = maybeLink;
1765
- return Boolean(link.sys?.id) && link.sys?.type === 'Link';
1706
+ const MAX_WIDTH_ALLOWED = 4000;
1707
+ const getOptimizedImageAsset = ({ file, sizes, loading, quality = '100%', format, }) => {
1708
+ const qualityNumber = Number(quality.replace('%', ''));
1709
+ if (!validateParams(file, qualityNumber, format)) ;
1710
+ const url = file.url;
1711
+ const maxWidth = Math.min(file.details.image.width, MAX_WIDTH_ALLOWED);
1712
+ const numOfParts = Math.max(2, Math.ceil(maxWidth / 500));
1713
+ const widthParts = Array.from({ length: numOfParts }, (_, index) => Math.ceil((index + 1) * (maxWidth / numOfParts)));
1714
+ const srcSet = sizes
1715
+ ? widthParts.map((width) => `${getOptimizedImageUrl(url, width, qualityNumber, format)} ${width}w`)
1716
+ : [];
1717
+ const intrinsicImageWidth = file.details.image.width;
1718
+ if (intrinsicImageWidth > MAX_WIDTH_ALLOWED) {
1719
+ srcSet.push(`${getOptimizedImageUrl(url, undefined, qualityNumber, format)} ${intrinsicImageWidth}w`);
1720
+ }
1721
+ const returnedUrl = getOptimizedImageUrl(url, file.details.image.width > 2000 ? 2000 : undefined, qualityNumber, format);
1722
+ const optimizedImageAsset = {
1723
+ url: returnedUrl,
1724
+ srcSet,
1725
+ sizes,
1726
+ file,
1727
+ loading,
1728
+ };
1729
+ return optimizedImageAsset;
1766
1730
  };
1767
1731
 
1768
- /**
1769
- * This module encapsulates format of the path to a deep reference.
1770
- */
1771
- const parseDataSourcePathIntoFieldset = (path) => {
1772
- const parsedPath = parseDeepPath(path);
1773
- if (null === parsedPath) {
1774
- throw new Error(`Cannot parse path '${path}' as deep path`);
1732
+ const transformMedia = (asset, variables, resolveDesignValue, variableName, path) => {
1733
+ let value;
1734
+ // If it is not a deep path and not pointing to the file of the asset,
1735
+ // it is just pointing to a normal field and therefore we just resolve the value as normal field
1736
+ if (!isDeepPath(path) && !lastPathNamedSegmentEq(path, 'file')) {
1737
+ return getBoundValue(asset, path);
1775
1738
  }
1776
- return parsedPath.fields.map((field) => [null, field, '~locale']);
1777
- };
1778
- /**
1779
- * Parse path into components, supports L1 references (one reference follow) atm.
1780
- * @param path from data source. eg. `/uuid123/fields/image/~locale/fields/file/~locale`
1781
- * eg. `/uuid123/fields/file/~locale/fields/title/~locale`
1782
- * @returns
1783
- */
1784
- const parseDataSourcePathWithL1DeepBindings = (path) => {
1785
- const parsedPath = parseDeepPath(path);
1786
- if (null === parsedPath) {
1787
- throw new Error(`Cannot parse path '${path}' as deep path`);
1739
+ //TODO: this will be better served by injectable type transformers instead of if statement
1740
+ if (variableName === 'cfImageAsset') {
1741
+ const optionsVariableName = 'cfImageOptions';
1742
+ const options = resolveDesignValue(variables[optionsVariableName]?.type === 'DesignValue'
1743
+ ? variables[optionsVariableName].valuesByBreakpoint
1744
+ : {}, optionsVariableName);
1745
+ if (!options) {
1746
+ console.error(`Error transforming image asset: Required variable [${optionsVariableName}] missing from component definition`);
1747
+ return;
1748
+ }
1749
+ try {
1750
+ value = getOptimizedImageAsset({
1751
+ file: asset.fields.file,
1752
+ loading: options.loading,
1753
+ sizes: options.targetSize,
1754
+ quality: options.quality,
1755
+ format: options.format,
1756
+ });
1757
+ return value;
1758
+ }
1759
+ catch (error) {
1760
+ console.error('Error transforming image asset', error);
1761
+ }
1762
+ return;
1788
1763
  }
1789
- return {
1790
- key: parsedPath.key,
1791
- field: parsedPath.fields[0],
1792
- referentField: parsedPath.fields[1],
1793
- };
1794
- };
1795
- /**
1796
- * Detects if paths is valid deep-path, like:
1797
- * - /gV6yKXp61hfYrR7rEyKxY/fields/mainStory/~locale/fields/cover/~locale/fields/title/~locale
1798
- * or regular, like:
1799
- * - /6J8eA60yXwdm5eyUh9fX6/fields/mainStory/~locale
1800
- * @returns
1801
- */
1802
- const isDeepPath = (deepPathCandidate) => {
1803
- const deepPathParsed = parseDeepPath(deepPathCandidate);
1804
- if (!deepPathParsed) {
1805
- return false;
1764
+ if (variableName === 'cfBackgroundImageUrl') {
1765
+ let width = resolveDesignValue(variables['cfWidth']?.type === 'DesignValue' ? variables['cfWidth'].valuesByBreakpoint : {}, 'cfWidth') || '100%';
1766
+ const optionsVariableName = 'cfBackgroundImageOptions';
1767
+ const options = resolveDesignValue(variables[optionsVariableName]?.type === 'DesignValue'
1768
+ ? variables[optionsVariableName].valuesByBreakpoint
1769
+ : {}, optionsVariableName);
1770
+ if (!options) {
1771
+ console.error(`Error transforming image asset: Required variable [${optionsVariableName}] missing from component definition`);
1772
+ return;
1773
+ }
1774
+ try {
1775
+ // Target width (px/rem/em) will be applied to the css url if it's lower than the original image width (in px)
1776
+ const assetDetails = asset.fields.file?.details;
1777
+ const assetWidth = assetDetails?.image?.width || 0; // This is always in px
1778
+ const targetWidthObject = parseCSSValue(options.targetSize); // Contains value and unit (px/rem/em) so convert and then compare to assetWidth
1779
+ const targetValue = targetWidthObject
1780
+ ? getTargetValueInPixels(targetWidthObject)
1781
+ : assetWidth;
1782
+ if (targetValue < assetWidth)
1783
+ width = `${targetValue}px`;
1784
+ value = getOptimizedBackgroundImageAsset(asset.fields.file, width, options.quality, options.format);
1785
+ return value;
1786
+ }
1787
+ catch (error) {
1788
+ console.error('Error transforming image asset', error);
1789
+ }
1790
+ return;
1806
1791
  }
1807
- return deepPathParsed.fields.length > 1;
1792
+ return asset.fields.file?.url;
1808
1793
  };
1809
- const parseDeepPath = (deepPathCandidate) => {
1810
- // ALGORITHM:
1811
- // We start with deep path in form:
1812
- // /uuid123/fields/mainStory/~locale/fields/cover/~locale/fields/title/~locale
1813
- // First turn string into array of segments
1814
- // ['', 'uuid123', 'fields', 'mainStory', '~locale', 'fields', 'cover', '~locale', 'fields', 'title', '~locale']
1815
- // Then group segments into intermediate represenatation - chunks, where each non-initial chunk starts with 'fields'
1816
- // [
1817
- // [ "", "uuid123" ],
1818
- // [ "fields", "mainStory", "~locale" ],
1819
- // [ "fields", "cover", "~locale" ],
1820
- // [ "fields", "title", "~locale" ]
1821
- // ]
1822
- // Then check "initial" chunk for corretness
1823
- // Then check all "field-leading" chunks for correctness
1824
- const isValidInitialChunk = (initialChunk) => {
1825
- // must have start with '' and have at least 2 segments, second non-empty
1826
- // eg. /-_432uuid123123
1827
- return /^\/([^/^~]+)$/.test(initialChunk.join('/'));
1828
- };
1829
- const isValidFieldChunk = (fieldChunk) => {
1830
- // must start with 'fields' and have at least 3 segments, second non-empty and last segment must be '~locale'
1831
- // eg. fields/-32234mainStory/~locale
1832
- return /^fields\/[^/^~]+\/~locale$/.test(fieldChunk.join('/'));
1833
- };
1834
- const deepPathSegments = deepPathCandidate.split('/');
1835
- const chunks = chunkSegments(deepPathSegments, { startNextChunkOnElementEqualTo: 'fields' });
1836
- if (chunks.length <= 1) {
1837
- return null; // malformed path, even regular paths have at least 2 chunks
1794
+
1795
+ function getResolvedEntryFromLink(entryOrAsset, path, entityStore) {
1796
+ if (entryOrAsset.sys.type === 'Asset') {
1797
+ return entryOrAsset;
1838
1798
  }
1839
- else if (chunks.length === 2) {
1840
- return null; // deep paths have at least 3 chunks
1799
+ const value = get(entryOrAsset, path.split('/').slice(2, -1));
1800
+ if (value?.sys.type !== 'Link') {
1801
+ console.warn(`Expected a link to a reference, but got: ${JSON.stringify(value)}`);
1802
+ return;
1841
1803
  }
1842
- // With 3+ chunks we can now check for deep path correctness
1843
- const [initialChunk, ...fieldChunks] = chunks;
1844
- if (!isValidInitialChunk(initialChunk)) {
1845
- return null;
1804
+ //Look up the reference in the entity store
1805
+ const resolvedEntity = entityStore.getEntityFromLink(value);
1806
+ if (!resolvedEntity) {
1807
+ return;
1846
1808
  }
1847
- if (!fieldChunks.every(isValidFieldChunk)) {
1848
- return null;
1809
+ //resolve any embedded links - we currently only support 2 levels deep
1810
+ const fields = resolvedEntity.fields || {};
1811
+ Object.entries(fields).forEach(([fieldKey, field]) => {
1812
+ if (field && field.sys?.type === 'Link') {
1813
+ const entity = entityStore.getEntityFromLink(field);
1814
+ if (entity) {
1815
+ resolvedEntity.fields[fieldKey] = entity;
1816
+ }
1817
+ }
1818
+ else if (field && Array.isArray(field)) {
1819
+ resolvedEntity.fields[fieldKey] = field.map((innerField) => {
1820
+ if (innerField && innerField.sys?.type === 'Link') {
1821
+ const entity = entityStore.getEntityFromLink(innerField);
1822
+ if (entity) {
1823
+ return entity;
1824
+ }
1825
+ }
1826
+ return innerField;
1827
+ });
1828
+ }
1829
+ });
1830
+ return resolvedEntity;
1831
+ }
1832
+
1833
+ function getArrayValue(entryOrAsset, path, entityStore) {
1834
+ if (entryOrAsset.sys.type === 'Asset') {
1835
+ return entryOrAsset;
1836
+ }
1837
+ const arrayValue = get(entryOrAsset, path.split('/').slice(2, -1));
1838
+ if (!isArray(arrayValue)) {
1839
+ console.warn(`Expected a value to be an array, but got: ${JSON.stringify(arrayValue)}`);
1840
+ return;
1849
1841
  }
1850
- return {
1851
- key: initialChunk[1], // pick uuid from initial chunk ['','uuid123'],
1852
- fields: fieldChunks.map((fieldChunk) => fieldChunk[1]), // pick only fieldName eg. from ['fields','mainStory', '~locale'] we pick `mainStory`
1853
- };
1854
- };
1855
- const chunkSegments = (segments, { startNextChunkOnElementEqualTo }) => {
1856
- const chunks = [];
1857
- let currentChunk = [];
1858
- const isSegmentBeginningOfChunk = (segment) => segment === startNextChunkOnElementEqualTo;
1859
- const excludeEmptyChunks = (chunk) => chunk.length > 0;
1860
- for (let i = 0; i < segments.length; i++) {
1861
- const isInitialElement = i === 0;
1862
- const segment = segments[i];
1863
- if (isInitialElement) {
1864
- currentChunk = [segment];
1842
+ const result = arrayValue.map((value) => {
1843
+ if (typeof value === 'string') {
1844
+ return value;
1865
1845
  }
1866
- else if (isSegmentBeginningOfChunk(segment)) {
1867
- chunks.push(currentChunk);
1868
- currentChunk = [segment];
1846
+ else if (value?.sys?.type === 'Link') {
1847
+ const resolvedEntity = entityStore.getEntityFromLink(value);
1848
+ if (!resolvedEntity) {
1849
+ return;
1850
+ }
1851
+ //resolve any embedded links - we currently only support 2 levels deep
1852
+ const fields = resolvedEntity.fields || {};
1853
+ Object.entries(fields).forEach(([fieldKey, field]) => {
1854
+ if (field && field.sys?.type === 'Link') {
1855
+ const entity = entityStore.getEntityFromLink(field);
1856
+ if (entity) {
1857
+ resolvedEntity.fields[fieldKey] = entity;
1858
+ }
1859
+ }
1860
+ });
1861
+ return resolvedEntity;
1869
1862
  }
1870
1863
  else {
1871
- currentChunk.push(segment);
1864
+ console.warn(`Expected value to be a string or Link, but got: ${JSON.stringify(value)}`);
1865
+ return undefined;
1872
1866
  }
1867
+ });
1868
+ return result;
1869
+ }
1870
+
1871
+ const transformBoundContentValue = (variables, entityStore, binding, resolveDesignValue, variableName, variableType, path) => {
1872
+ const entityOrAsset = entityStore.getEntryOrAsset(binding, path);
1873
+ if (!entityOrAsset)
1874
+ return;
1875
+ switch (variableType) {
1876
+ case 'Media':
1877
+ // If we bound a normal entry field to the media variable we just return the bound value
1878
+ if (entityOrAsset.sys.type === 'Entry') {
1879
+ return getBoundValue(entityOrAsset, path);
1880
+ }
1881
+ return transformMedia(entityOrAsset, variables, resolveDesignValue, variableName, path);
1882
+ case 'RichText':
1883
+ return transformRichText(entityOrAsset, entityStore, path);
1884
+ case 'Array':
1885
+ return getArrayValue(entityOrAsset, path, entityStore);
1886
+ case 'Link':
1887
+ return getResolvedEntryFromLink(entityOrAsset, path, entityStore);
1888
+ default:
1889
+ return getBoundValue(entityOrAsset, path);
1873
1890
  }
1874
- chunks.push(currentChunk);
1875
- return chunks.filter(excludeEmptyChunks);
1876
- };
1877
- const lastPathNamedSegmentEq = (path, expectedName) => {
1878
- // `/key123/fields/featureImage/~locale/fields/file/~locale`
1879
- // ['', 'key123', 'fields', 'featureImage', '~locale', 'fields', 'file', '~locale']
1880
- const segments = path.split('/');
1881
- if (segments.length < 2) {
1882
- console.warn(`[experiences-sdk-react] Attempting to check whether last named segment of the path (${path}) equals to '${expectedName}', but the path doesn't have enough segments.`);
1883
- return false;
1884
- }
1885
- const secondLast = segments[segments.length - 2]; // skipping trailing '~locale'
1886
- return secondLast === expectedName;
1887
1891
  };
1888
1892
 
1889
- const resolveHyperlinkPattern = (pattern, entry, locale) => {
1890
- if (!entry || !locale)
1891
- return null;
1892
- const variables = {
1893
- entry,
1894
- locale,
1893
+ const getDataFromTree = (tree) => {
1894
+ let dataSource = {};
1895
+ let unboundValues = {};
1896
+ const queue = [...tree.root.children];
1897
+ while (queue.length) {
1898
+ const node = queue.shift();
1899
+ if (!node) {
1900
+ continue;
1901
+ }
1902
+ dataSource = { ...dataSource, ...node.data.dataSource };
1903
+ unboundValues = { ...unboundValues, ...node.data.unboundValues };
1904
+ if (node.children.length) {
1905
+ queue.push(...node.children);
1906
+ }
1907
+ }
1908
+ return {
1909
+ dataSource,
1910
+ unboundValues,
1895
1911
  };
1896
- return buildTemplate({ template: pattern, context: variables });
1897
1912
  };
1898
- function getValue(obj, path) {
1899
- return path
1900
- .replace(/\[/g, '.')
1901
- .replace(/\]/g, '')
1902
- .split('.')
1903
- .reduce((o, k) => (o || {})[k], obj);
1904
- }
1905
- function addLocale(str, locale) {
1906
- const fieldsIndicator = 'fields';
1907
- const fieldsIndex = str.indexOf(fieldsIndicator);
1908
- if (fieldsIndex !== -1) {
1909
- const dotIndex = str.indexOf('.', fieldsIndex + fieldsIndicator.length + 1); // +1 for '.'
1910
- if (dotIndex !== -1) {
1911
- return str.slice(0, dotIndex + 1) + locale + '.' + str.slice(dotIndex + 1);
1912
- }
1913
+ function parseCSSValue(input) {
1914
+ const regex = /^(\d+(\.\d+)?)(px|em|rem)$/;
1915
+ const match = input.match(regex);
1916
+ if (match) {
1917
+ return {
1918
+ value: parseFloat(match[1]),
1919
+ unit: match[3],
1920
+ };
1913
1921
  }
1914
- return str;
1915
- }
1916
- function getTemplateValue(ctx, path) {
1917
- const pathWithLocale = addLocale(path, ctx.locale);
1918
- const retrievedValue = getValue(ctx, pathWithLocale);
1919
- return typeof retrievedValue === 'object' && retrievedValue !== null
1920
- ? retrievedValue[ctx.locale]
1921
- : retrievedValue;
1922
+ return null;
1922
1923
  }
1923
- function buildTemplate({ template, context, }) {
1924
- const localeVariable = /{\s*locale\s*}/g;
1925
- // e.g. "{ page.sys.id }"
1926
- const variables = /{\s*([\S]+?)\s*}/g;
1927
- return (template
1928
- // first replace the locale pattern
1929
- .replace(localeVariable, context.locale)
1930
- // then resolve the remaining variables
1931
- .replace(variables, (_, path) => {
1932
- const fallback = path + '_NOT_FOUND';
1933
- const value = getTemplateValue(context, path) ?? fallback;
1934
- // using _.result didn't gave proper results so we run our own version of it
1935
- return String(typeof value === 'function' ? value() : value);
1936
- }));
1924
+ function getTargetValueInPixels(targetWidthObject) {
1925
+ switch (targetWidthObject.unit) {
1926
+ case 'px':
1927
+ return targetWidthObject.value;
1928
+ case 'em':
1929
+ return targetWidthObject.value * 16;
1930
+ case 'rem':
1931
+ return targetWidthObject.value * 16;
1932
+ default:
1933
+ return targetWidthObject.value;
1934
+ }
1937
1935
  }
1938
1936
 
1939
- const stylesToKeep = ['cfImageAsset'];
1940
- const stylesToRemove = CF_STYLE_ATTRIBUTES.filter((style) => !stylesToKeep.includes(style));
1941
- const propsToRemove = ['cfHyperlink', 'cfOpenInNewTab', 'cfSsrClassName'];
1942
- const sanitizeNodeProps = (nodeProps) => {
1943
- return omit(nodeProps, stylesToRemove, propsToRemove);
1944
- };
1945
-
1946
- const CF_DEBUG_KEY$1 = 'cf_debug';
1947
- let DebugLogger$1 = class DebugLogger {
1948
- constructor() {
1949
- // Public methods for logging
1950
- this.error = this.logger('error');
1951
- this.warn = this.logger('warn');
1952
- this.log = this.logger('log');
1953
- this.debug = this.logger('debug');
1954
- if (typeof localStorage === 'undefined') {
1955
- this.enabled = false;
1956
- return;
1957
- }
1958
- // Default to checking localStorage for the debug mode on initialization if in browser
1959
- this.enabled = localStorage.getItem(CF_DEBUG_KEY$1) === 'true';
1937
+ class ParseError extends Error {
1938
+ constructor(message) {
1939
+ super(message);
1960
1940
  }
1961
- static getInstance() {
1962
- if (this.instance === null) {
1963
- this.instance = new DebugLogger();
1941
+ }
1942
+ const isValidJsonObject = (s) => {
1943
+ try {
1944
+ const result = JSON.parse(s);
1945
+ if ('object' !== typeof result) {
1946
+ return false;
1964
1947
  }
1965
- return this.instance;
1948
+ return true;
1966
1949
  }
1967
- getEnabled() {
1968
- return this.enabled;
1950
+ catch (e) {
1951
+ return false;
1969
1952
  }
1970
- setEnabled(enabled) {
1971
- this.enabled = enabled;
1972
- if (typeof localStorage === 'undefined') {
1973
- return;
1974
- }
1975
- if (enabled) {
1976
- localStorage.setItem(CF_DEBUG_KEY$1, 'true');
1977
- }
1978
- else {
1979
- localStorage.removeItem(CF_DEBUG_KEY$1);
1953
+ };
1954
+ const doesMismatchMessageSchema = (event) => {
1955
+ try {
1956
+ tryParseMessage(event);
1957
+ return false;
1958
+ }
1959
+ catch (e) {
1960
+ if (e instanceof ParseError) {
1961
+ return e.message;
1980
1962
  }
1963
+ throw e;
1981
1964
  }
1982
- // Log method for different levels (error, warn, log)
1983
- logger(level) {
1984
- return (...args) => {
1985
- if (this.enabled) {
1986
- console[level]('[cf-experiences-sdk]', ...args);
1987
- }
1988
- };
1965
+ };
1966
+ const tryParseMessage = (event) => {
1967
+ if (!event.data) {
1968
+ throw new ParseError('Field event.data is missing');
1969
+ }
1970
+ if ('string' !== typeof event.data) {
1971
+ throw new ParseError(`Field event.data must be a string, instead of '${typeof event.data}'`);
1972
+ }
1973
+ if (!isValidJsonObject(event.data)) {
1974
+ throw new ParseError('Field event.data must be a valid JSON object serialized as string');
1975
+ }
1976
+ const eventData = JSON.parse(event.data);
1977
+ if (!eventData.source) {
1978
+ throw new ParseError(`Field eventData.source must be equal to 'composability-app'`);
1979
+ }
1980
+ if ('composability-app' !== eventData.source) {
1981
+ throw new ParseError(`Field eventData.source must be equal to 'composability-app', instead of '${eventData.source}'`);
1982
+ }
1983
+ // check eventData.eventType
1984
+ const supportedEventTypes = Object.values(INCOMING_EVENTS$1);
1985
+ if (!supportedEventTypes.includes(eventData.eventType)) {
1986
+ // Expected message: This message is handled in the EntityStore to store fetched entities
1987
+ if (eventData.eventType !== PostMessageMethods$3.REQUESTED_ENTITIES) {
1988
+ throw new ParseError(`Field eventData.eventType must be one of the supported values: [${supportedEventTypes.join(', ')}]`);
1989
+ }
1989
1990
  }
1991
+ return eventData;
1990
1992
  };
1991
- DebugLogger$1.instance = null;
1992
- DebugLogger$1.getInstance();
1993
1993
 
1994
1994
  const sendMessage = (eventType, data) => {
1995
1995
  if (typeof window === 'undefined') {