@contentful/experiences-core 1.28.0-dev-20250115T1128-04df9c2.0 → 1.28.0-prerelease-20250115T2332-646080a.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,7 +1,7 @@
1
- import md5 from 'md5';
2
1
  import { z, ZodIssueCode } from 'zod';
2
+ import { omit, isArray, uniqBy } from 'lodash-es';
3
+ import md5 from 'md5';
3
4
  import { BLOCKS } from '@contentful/rich-text-types';
4
- import { isArray, omit, uniqBy } from 'lodash-es';
5
5
 
6
6
  const INCOMING_EVENTS = {
7
7
  RequestEditorMode: 'requestEditorMode',
@@ -193,243 +193,14 @@ const getElementCoordinates = (element) => {
193
193
  });
194
194
  };
195
195
 
196
- class ParseError extends Error {
197
- constructor(message) {
198
- super(message);
199
- }
200
- }
201
- const isValidJsonObject = (s) => {
202
- try {
203
- const result = JSON.parse(s);
204
- if ('object' !== typeof result) {
205
- return false;
206
- }
207
- return true;
208
- }
209
- catch (e) {
210
- return false;
211
- }
212
- };
213
- const doesMismatchMessageSchema = (event) => {
214
- try {
215
- tryParseMessage(event);
216
- return false;
217
- }
218
- catch (e) {
219
- if (e instanceof ParseError) {
220
- return e.message;
221
- }
222
- throw e;
223
- }
224
- };
225
- const tryParseMessage = (event) => {
226
- if (!event.data) {
227
- throw new ParseError('Field event.data is missing');
228
- }
229
- if ('string' !== typeof event.data) {
230
- throw new ParseError(`Field event.data must be a string, instead of '${typeof event.data}'`);
231
- }
232
- if (!isValidJsonObject(event.data)) {
233
- throw new ParseError('Field event.data must be a valid JSON object serialized as string');
234
- }
235
- const eventData = JSON.parse(event.data);
236
- if (!eventData.source) {
237
- throw new ParseError(`Field eventData.source must be equal to 'composability-app'`);
238
- }
239
- if ('composability-app' !== eventData.source) {
240
- throw new ParseError(`Field eventData.source must be equal to 'composability-app', instead of '${eventData.source}'`);
241
- }
242
- // check eventData.eventType
243
- const supportedEventTypes = Object.values(INCOMING_EVENTS);
244
- if (!supportedEventTypes.includes(eventData.eventType)) {
245
- // Expected message: This message is handled in the EntityStore to store fetched entities
246
- if (eventData.eventType !== PostMessageMethods.REQUESTED_ENTITIES) {
247
- throw new ParseError(`Field eventData.eventType must be one of the supported values: [${supportedEventTypes.join(', ')}]`);
248
- }
249
- }
250
- return eventData;
251
- };
252
- const validateExperienceBuilderConfig = ({ locale, mode, }) => {
253
- if (mode === StudioCanvasMode.EDITOR || mode === StudioCanvasMode.READ_ONLY) {
254
- return;
255
- }
256
- if (!locale) {
257
- throw new Error('Parameter "locale" is required for experience builder initialization outside of editor mode');
258
- }
259
- };
260
-
261
- const transformVisibility = (value) => {
262
- if (value === false) {
263
- return {
264
- display: 'none',
265
- };
266
- }
267
- // Don't explicitly set anything when visible to not overwrite values like `grid` or `flex`.
268
- return {};
269
- };
270
- // Keep this for backwards compatibility - deleting this would be a breaking change
271
- // because existing components on a users experience will have the width value as fill
272
- // rather than 100%
273
- const transformFill = (value) => (value === 'fill' ? '100%' : value);
274
- const transformGridColumn = (span) => {
275
- if (!span) {
276
- return {};
277
- }
278
- return {
279
- gridColumn: `span ${span}`,
280
- };
281
- };
282
- const transformBorderStyle = (value) => {
283
- if (!value)
284
- return {};
285
- const parts = value.split(' ');
286
- // Just accept the passed value
287
- if (parts.length < 3)
288
- return { border: value };
289
- const [borderSize, borderStyle, ...borderColorParts] = parts;
290
- const borderColor = borderColorParts.join(' ');
291
- return {
292
- border: `${borderSize} ${borderStyle} ${borderColor}`,
293
- };
294
- };
295
- const transformAlignment = (cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection = 'column') => cfFlexDirection === 'row'
296
- ? {
297
- alignItems: cfHorizontalAlignment,
298
- justifyContent: cfVerticalAlignment === 'center' ? `safe ${cfVerticalAlignment}` : cfVerticalAlignment,
299
- }
300
- : {
301
- alignItems: cfVerticalAlignment,
302
- justifyContent: cfHorizontalAlignment === 'center'
303
- ? `safe ${cfHorizontalAlignment}`
304
- : cfHorizontalAlignment,
305
- };
306
- const transformBackgroundImage = (cfBackgroundImageUrl, cfBackgroundImageOptions) => {
307
- const matchBackgroundSize = (scaling) => {
308
- if ('fill' === scaling)
309
- return 'cover';
310
- if ('fit' === scaling)
311
- return 'contain';
312
- };
313
- const matchBackgroundPosition = (alignment) => {
314
- if (!alignment || 'string' !== typeof alignment) {
315
- return;
316
- }
317
- let [horizontalAlignment, verticalAlignment] = alignment.trim().split(/\s+/, 2);
318
- // Special case for handling single values
319
- // for backwards compatibility with single values 'right','left', 'center', 'top','bottom'
320
- if (horizontalAlignment && !verticalAlignment) {
321
- const singleValue = horizontalAlignment;
322
- switch (singleValue) {
323
- case 'left':
324
- horizontalAlignment = 'left';
325
- verticalAlignment = 'center';
326
- break;
327
- case 'right':
328
- horizontalAlignment = 'right';
329
- verticalAlignment = 'center';
330
- break;
331
- case 'center':
332
- horizontalAlignment = 'center';
333
- verticalAlignment = 'center';
334
- break;
335
- case 'top':
336
- horizontalAlignment = 'center';
337
- verticalAlignment = 'top';
338
- break;
339
- case 'bottom':
340
- horizontalAlignment = 'center';
341
- verticalAlignment = 'bottom';
342
- break;
343
- // just fall down to the normal validation logic for horiz and vert
344
- }
345
- }
346
- const isHorizontalValid = ['left', 'right', 'center'].includes(horizontalAlignment);
347
- const isVerticalValid = ['top', 'bottom', 'center'].includes(verticalAlignment);
348
- horizontalAlignment = isHorizontalValid ? horizontalAlignment : 'left';
349
- verticalAlignment = isVerticalValid ? verticalAlignment : 'top';
350
- return `${horizontalAlignment} ${verticalAlignment}`;
351
- };
352
- if (!cfBackgroundImageUrl) {
353
- return;
354
- }
355
- let backgroundImage;
356
- let backgroundImageSet;
357
- if (typeof cfBackgroundImageUrl === 'string') {
358
- backgroundImage = `url(${cfBackgroundImageUrl})`;
359
- }
360
- else {
361
- const imgSet = cfBackgroundImageUrl.srcSet?.join(',');
362
- backgroundImage = `url(${cfBackgroundImageUrl.url})`;
363
- backgroundImageSet = `image-set(${imgSet})`;
364
- }
365
- return {
366
- backgroundImage,
367
- backgroundImage2: backgroundImageSet,
368
- backgroundRepeat: cfBackgroundImageOptions?.scaling === 'tile' ? 'repeat' : 'no-repeat',
369
- backgroundPosition: matchBackgroundPosition(cfBackgroundImageOptions?.alignment),
370
- backgroundSize: matchBackgroundSize(cfBackgroundImageOptions?.scaling),
371
- };
372
- };
373
-
374
- const toCSSAttribute = (key) => {
375
- let val = key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
376
- // Remove the number from the end of the key to allow for overrides on style properties
377
- val = val.replace(/\d+$/, '');
378
- return val;
379
- };
380
- const buildStyleTag = ({ styles, nodeId }) => {
381
- const stylesStr = Object.entries(styles)
382
- .filter(([, value]) => value !== undefined)
383
- .reduce((acc, [key, value]) => `${acc}
384
- ${toCSSAttribute(key)}: ${value};`, '');
385
- const className = `cfstyles-${nodeId ? nodeId : md5(stylesStr)}`;
386
- const styleRule = `.${className}{ ${stylesStr} }`;
387
- return [className, styleRule];
388
- };
389
- const buildCfStyles = ({ cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection, cfFlexReverse, cfFlexWrap, cfMargin, cfPadding, cfBackgroundColor, cfWidth, cfHeight, cfMaxWidth, cfBorder, cfBorderRadius, cfGap, cfBackgroundImageUrl, cfBackgroundImageOptions, cfFontSize, cfFontWeight, cfImageOptions, cfLineHeight, cfLetterSpacing, cfTextColor, cfTextAlign, cfTextTransform, cfTextBold, cfTextItalic, cfTextUnderline, cfColumnSpan, cfVisibility, }) => {
390
- return {
391
- boxSizing: 'border-box',
392
- ...transformVisibility(cfVisibility),
393
- margin: cfMargin,
394
- padding: cfPadding,
395
- backgroundColor: cfBackgroundColor,
396
- width: transformFill(cfWidth || cfImageOptions?.width),
397
- height: transformFill(cfHeight || cfImageOptions?.height),
398
- maxWidth: cfMaxWidth,
399
- ...transformGridColumn(cfColumnSpan),
400
- ...transformBorderStyle(cfBorder),
401
- borderRadius: cfBorderRadius,
402
- gap: cfGap,
403
- ...transformAlignment(cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection),
404
- flexDirection: cfFlexReverse && cfFlexDirection ? `${cfFlexDirection}-reverse` : cfFlexDirection,
405
- flexWrap: cfFlexWrap,
406
- ...transformBackgroundImage(cfBackgroundImageUrl, cfBackgroundImageOptions),
407
- fontSize: cfFontSize,
408
- fontWeight: cfTextBold ? 'bold' : cfFontWeight,
409
- fontStyle: cfTextItalic ? 'italic' : undefined,
410
- textDecoration: cfTextUnderline ? 'underline' : undefined,
411
- lineHeight: cfLineHeight,
412
- letterSpacing: cfLetterSpacing,
413
- color: cfTextColor,
414
- textAlign: cfTextAlign,
415
- textTransform: cfTextTransform,
416
- objectFit: cfImageOptions?.objectFit,
417
- objectPosition: cfImageOptions?.objectPosition,
418
- };
419
- };
420
- /**
421
- * Container/section default behavior:
422
- * Default height => height: EMPTY_CONTAINER_HEIGHT
423
- * If a container component has children => height: 'fit-content'
424
- */
425
- const calculateNodeDefaultHeight = ({ blockId, children, value, }) => {
426
- if (!blockId || !isContentfulStructureComponent(blockId) || value !== 'auto') {
427
- return value;
428
- }
429
- if (children.length) {
430
- return '100%';
431
- }
432
- return EMPTY_CONTAINER_HEIGHT;
196
+ const isExperienceEntry = (entry) => {
197
+ return (entry?.sys?.type === 'Entry' &&
198
+ !!entry.fields?.title &&
199
+ !!entry.fields?.slug &&
200
+ !!entry.fields?.componentTree &&
201
+ Array.isArray(entry.fields.componentTree.breakpoints) &&
202
+ Array.isArray(entry.fields.componentTree.children) &&
203
+ typeof entry.fields.componentTree.schemaVersion === 'string');
433
204
  };
434
205
 
435
206
  // These styles get added to every component, user custom or contentful provided
@@ -1509,478 +1280,553 @@ const resetBreakpointsRegistry = () => {
1509
1280
  breakpointsRegistry = [];
1510
1281
  };
1511
1282
 
1512
- const detachExperienceStyles = (experience) => {
1513
- const experienceTreeRoot = experience.entityStore?.experienceEntryFields
1514
- ?.componentTree;
1515
- if (!experienceTreeRoot) {
1516
- return;
1517
- }
1518
- const mapOfDesignVariableKeys = flattenDesignTokenRegistry(designTokensRegistry);
1519
- // getting breakpoints from the entry componentTree field
1520
- /**
1521
- * breakpoints [
1522
- {
1523
- id: 'desktop',
1524
- query: '*',
1525
- displayName: 'All Sizes',
1526
- previewSize: '100%'
1527
- },
1528
- {
1529
- id: 'tablet',
1530
- query: '<992px',
1531
- displayName: 'Tablet',
1532
- previewSize: '820px'
1533
- },
1534
- {
1535
- id: 'mobile',
1536
- query: '<576px',
1537
- displayName: 'Mobile',
1538
- previewSize: '390px'
1283
+ const MEDIA_QUERY_REGEXP = /(<|>)(\d{1,})(px|cm|mm|in|pt|pc)$/;
1284
+ const toCSSMediaQuery = ({ query }) => {
1285
+ if (query === '*')
1286
+ return undefined;
1287
+ const match = query.match(MEDIA_QUERY_REGEXP);
1288
+ if (!match)
1289
+ return undefined;
1290
+ const [, operator, value, unit] = match;
1291
+ if (operator === '<') {
1292
+ const maxScreenWidth = Number(value) - 1;
1293
+ return `(max-width: ${maxScreenWidth}${unit})`;
1294
+ }
1295
+ else if (operator === '>') {
1296
+ const minScreenWidth = Number(value) + 1;
1297
+ return `(min-width: ${minScreenWidth}${unit})`;
1298
+ }
1299
+ return undefined;
1300
+ };
1301
+ // Remove this helper when upgrading to TypeScript 5.0 - https://github.com/microsoft/TypeScript/issues/48829
1302
+ const findLast = (array, predicate) => {
1303
+ return array.reverse().find(predicate);
1304
+ };
1305
+ // Initialise media query matchers. This won't include the always matching fallback breakpoint.
1306
+ const mediaQueryMatcher = (breakpoints) => {
1307
+ const mediaQueryMatches = {};
1308
+ const mediaQueryMatchers = breakpoints
1309
+ .map((breakpoint) => {
1310
+ const cssMediaQuery = toCSSMediaQuery(breakpoint);
1311
+ if (!cssMediaQuery)
1312
+ return undefined;
1313
+ if (typeof window === 'undefined')
1314
+ return undefined;
1315
+ const mediaQueryMatcher = window.matchMedia(cssMediaQuery);
1316
+ mediaQueryMatches[breakpoint.id] = mediaQueryMatcher.matches;
1317
+ return { id: breakpoint.id, signal: mediaQueryMatcher };
1318
+ })
1319
+ .filter((matcher) => !!matcher);
1320
+ return [mediaQueryMatchers, mediaQueryMatches];
1321
+ };
1322
+ const getActiveBreakpointIndex = (breakpoints, mediaQueryMatches, fallbackBreakpointIndex) => {
1323
+ // The breakpoints are ordered (desktop-first: descending by screen width)
1324
+ const breakpointsWithMatches = breakpoints.map(({ id }, index) => ({
1325
+ id,
1326
+ index,
1327
+ // The fallback breakpoint with wildcard query will always match
1328
+ isMatch: mediaQueryMatches[id] ?? index === fallbackBreakpointIndex,
1329
+ }));
1330
+ // Find the last breakpoint in the list that matches (desktop-first: the narrowest one)
1331
+ const mostSpecificIndex = findLast(breakpointsWithMatches, ({ isMatch }) => isMatch)?.index;
1332
+ return mostSpecificIndex ?? fallbackBreakpointIndex;
1333
+ };
1334
+ const getFallbackBreakpointIndex = (breakpoints) => {
1335
+ // We assume that there will be a single breakpoint which uses the wildcard query.
1336
+ // If there is none, we just take the first one in the list.
1337
+ return Math.max(breakpoints.findIndex(({ query }) => query === '*'), 0);
1338
+ };
1339
+ const builtInStylesWithDesignTokens = [
1340
+ 'cfMargin',
1341
+ 'cfPadding',
1342
+ 'cfGap',
1343
+ 'cfWidth',
1344
+ 'cfHeight',
1345
+ 'cfBackgroundColor',
1346
+ 'cfBorder',
1347
+ 'cfBorderRadius',
1348
+ 'cfFontSize',
1349
+ 'cfLineHeight',
1350
+ 'cfLetterSpacing',
1351
+ 'cfTextColor',
1352
+ 'cfMaxWidth',
1353
+ ];
1354
+ const isValidBreakpointValue = (value) => {
1355
+ return value !== undefined && value !== null && value !== '';
1356
+ };
1357
+ const getValueForBreakpoint = (valuesByBreakpoint, breakpoints, activeBreakpointIndex, variableName, resolveDesignTokens = true) => {
1358
+ const eventuallyResolveDesignTokens = (value) => {
1359
+ // For some built-in design properties, we support design tokens
1360
+ if (builtInStylesWithDesignTokens.includes(variableName)) {
1361
+ return getDesignTokenRegistration(value, variableName);
1539
1362
  }
1540
- ]
1541
- */
1542
- const { breakpoints } = experienceTreeRoot;
1543
- // creating the structure which I thought would work best for aggregation
1544
- const mediaQueriesTemplate = breakpoints.reduce((mediaQueryTemplate, breakpoint) => {
1545
- return {
1546
- ...mediaQueryTemplate,
1547
- [breakpoint.id]: {
1548
- condition: breakpoint.query,
1549
- cssByClassName: {},
1550
- },
1551
- };
1552
- }, {});
1553
- // getting the breakpoint ids
1554
- const breakpointIds = Object.keys(mediaQueriesTemplate);
1555
- const iterateOverTreeAndExtractStyles = ({ componentTree, dataSource, unboundValues, componentSettings, componentVariablesOverwrites, patternWrapper, }) => {
1556
- // traversing the tree
1557
- const queue = [];
1558
- queue.push(...componentTree.children);
1559
- let currentNode = undefined;
1560
- // for each tree node
1561
- while (queue.length) {
1562
- currentNode = queue.shift();
1563
- if (!currentNode) {
1564
- break;
1565
- }
1566
- const usedComponents = experience.entityStore?.usedComponents ?? [];
1567
- const isPatternNode = checkIsAssemblyNode({
1568
- componentId: currentNode.definitionId,
1569
- usedComponents,
1570
- });
1571
- if (isPatternNode) {
1572
- const patternEntry = usedComponents.find((component) => component.sys.id === currentNode.definitionId);
1573
- if (!patternEntry || !('fields' in patternEntry)) {
1574
- continue;
1575
- }
1576
- const defaultPatternDivStyles = Object.fromEntries(Object.entries(buildCfStyles({}))
1577
- .filter(([, value]) => value !== undefined)
1578
- .map(([key, value]) => [toCSSAttribute(key), value]));
1579
- // I create a hash of the object above because that would ensure hash stability
1580
- const styleHash = md5(JSON.stringify(defaultPatternDivStyles));
1581
- // and prefix the className to make sure the value can be processed
1582
- const className = `cf-${styleHash}`;
1583
- for (const breakpointId of breakpointIds) {
1584
- if (!mediaQueriesTemplate[breakpointId].cssByClassName[className]) {
1585
- mediaQueriesTemplate[breakpointId].cssByClassName[className] =
1586
- toCSSString(defaultPatternDivStyles);
1587
- }
1588
- }
1589
- currentNode.variables.cfSsrClassName = {
1590
- type: 'DesignValue',
1591
- valuesByBreakpoint: {
1592
- [breakpointIds[0]]: className,
1593
- },
1594
- };
1595
- // the node of a used pattern contains only the definitionId (id of the patter entry)
1596
- // as well as the variables overwrites
1597
- // the layout of a pattern is stored in it's entry
1598
- iterateOverTreeAndExtractStyles({
1599
- // that is why we pass it here to iterate of the pattern tree
1600
- componentTree: patternEntry.fields.componentTree,
1601
- // but we pass the data source of the experience entry cause that's where the binding is stored
1602
- dataSource,
1603
- // unbound values of a pattern store the default values of pattern variables
1604
- unboundValues: patternEntry.fields.unboundValues,
1605
- // this is where we can map the pattern variable to it's default value
1606
- componentSettings: patternEntry.fields.componentSettings,
1607
- // and this is where the over-writes for the default values are stored
1608
- // yes, I know, it's a bit confusing
1609
- componentVariablesOverwrites: currentNode.variables,
1610
- // pass top-level pattern node to store instance-specific child styles for rendering
1611
- patternWrapper: currentNode,
1612
- });
1613
- continue;
1614
- }
1615
- /** Variables value is stored in `valuesByBreakpoint` object
1616
- * {
1617
- cfVerticalAlignment: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'center' } },
1618
- cfHorizontalAlignment: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'center' } },
1619
- cfMargin: { type: 'DesignValue', valuesByBreakpoint: { desktop: '0 0 0 0' } },
1620
- cfPadding: { type: 'DesignValue', valuesByBreakpoint: { desktop: '0 0 0 0' } },
1621
- cfBackgroundColor: {
1622
- type: 'DesignValue',
1623
- valuesByBreakpoint: { desktop: 'rgba(246, 246, 246, 1)' }
1624
- },
1625
- cfWidth: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'fill' } },
1626
- cfHeight: {
1627
- type: 'DesignValue',
1628
- valuesByBreakpoint: { desktop: 'fit-content' }
1629
- },
1630
- cfMaxWidth: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'none' } },
1631
- cfFlexDirection: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'column' } },
1632
- cfFlexWrap: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'nowrap' } },
1633
- cfBorder: {
1634
- type: 'DesignValue',
1635
- valuesByBreakpoint: { desktop: '0px solid rgba(0, 0, 0, 0)' }
1636
- },
1637
- cfBorderRadius: { type: 'DesignValue', valuesByBreakpoint: { desktop: '0px' } },
1638
- cfGap: { type: 'DesignValue', valuesByBreakpoint: { desktop: '0px 0px' } },
1639
- cfHyperlink: { type: 'UnboundValue', key: 'VNc49Qyepd6IzN7rmKUyS' },
1640
- cfOpenInNewTab: { type: 'UnboundValue', key: 'ZA5YqB2fmREQ4pTKqY5hX' },
1641
- cfBackgroundImageUrl: { type: 'UnboundValue', key: 'FeskH0WbYD5_RQVXX-1T8' },
1642
- cfBackgroundImageOptions: { type: 'DesignValue', valuesByBreakpoint: { desktop: [Object] } }
1643
- }
1644
- */
1645
- // so first, I convert it into a map to help me make it easier to access the values
1646
- const propsByBreakpoint = indexByBreakpoint({
1647
- variables: currentNode.variables,
1648
- breakpointIds,
1649
- unboundValues: unboundValues,
1650
- dataSource: dataSource,
1651
- componentSettings,
1652
- componentVariablesOverwrites,
1653
- getBoundEntityById: (id) => {
1654
- return experience.entityStore?.entities.find((entity) => entity.sys.id === id);
1655
- },
1656
- });
1657
- /**
1658
- * propsByBreakpoint {
1659
- desktop: {
1660
- cfVerticalAlignment: 'center',
1661
- cfHorizontalAlignment: 'center',
1662
- cfMargin: '0 0 0 0',
1663
- cfPadding: '0 0 0 0',
1664
- cfBackgroundColor: 'rgba(246, 246, 246, 1)',
1665
- cfWidth: 'fill',
1666
- cfHeight: 'fit-content',
1667
- cfMaxWidth: 'none',
1668
- cfFlexDirection: 'column',
1669
- cfFlexWrap: 'nowrap',
1670
- cfBorder: '0px solid rgba(0, 0, 0, 0)',
1671
- cfBorderRadius: '0px',
1672
- cfGap: '0px 0px',
1673
- cfBackgroundImageOptions: { scaling: 'fill', alignment: 'left top', targetSize: '2000px' }
1674
- },
1675
- tablet: {},
1676
- mobile: {}
1677
- }
1678
- */
1679
- const currentNodeClassNames = [];
1680
- // then for each breakpoint
1681
- for (const breakpointId of breakpointIds) {
1682
- const propsByBreakpointWithResolvedDesignTokens = Object.entries(propsByBreakpoint[breakpointId]).reduce((acc, [variableName, variableValue]) => {
1683
- return {
1684
- ...acc,
1685
- [variableName]: maybePopulateDesignTokenValue(variableName, variableValue, mapOfDesignVariableKeys),
1686
- };
1687
- }, {});
1688
- // We convert cryptic prop keys to css variables
1689
- // Eg: cfMargin to margin
1690
- const stylesForBreakpoint = buildCfStyles(propsByBreakpointWithResolvedDesignTokens);
1691
- const stylesForBreakpointWithoutUndefined = Object.fromEntries(Object.entries(stylesForBreakpoint)
1692
- .filter(([, value]) => value !== undefined)
1693
- .map(([key, value]) => [toCSSAttribute(key), value]));
1694
- /**
1695
- * stylesForBreakpoint {
1696
- margin: '0 0 0 0',
1697
- padding: '0 0 0 0',
1698
- 'background-color': 'rgba(246, 246, 246, 1)',
1699
- width: '100%',
1700
- height: 'fit-content',
1701
- 'max-width': 'none',
1702
- border: '0px solid rgba(0, 0, 0, 0)',
1703
- 'border-radius': '0px',
1704
- gap: '0px 0px',
1705
- 'align-items': 'center',
1706
- 'justify-content': 'safe center',
1707
- 'flex-direction': 'column',
1708
- 'flex-wrap': 'nowrap',
1709
- 'font-style': 'normal',
1710
- 'text-decoration': 'none',
1711
- 'box-sizing': 'border-box'
1712
- }
1713
- */
1714
- // I create a hash of the object above because that would ensure hash stability
1715
- const styleHash = md5(JSON.stringify(stylesForBreakpointWithoutUndefined));
1716
- // and prefix the className to make sure the value can be processed
1717
- const className = `cf-${styleHash}`;
1718
- // I save the generated hashes into an array to later save it in the tree node
1719
- // as cfSsrClassName prop
1720
- // making sure to avoid the duplicates in case styles for > 1 breakpoints are the same
1721
- if (!currentNodeClassNames.includes(className)) {
1722
- currentNodeClassNames.push(className);
1723
- }
1724
- // if there is already the similar hash - no need to over-write it
1725
- if (mediaQueriesTemplate[breakpointId].cssByClassName[className]) {
1726
- continue;
1363
+ // For all other properties, we just return the breakpoint-specific value
1364
+ return value;
1365
+ };
1366
+ if (valuesByBreakpoint instanceof Object) {
1367
+ // Assume that the values are sorted by media query to apply the cascading CSS logic
1368
+ for (let index = activeBreakpointIndex; index >= 0; index--) {
1369
+ const breakpointId = breakpoints[index]?.id;
1370
+ if (isValidBreakpointValue(valuesByBreakpoint[breakpointId])) {
1371
+ // If the value is defined, we use it and stop the breakpoints cascade
1372
+ if (resolveDesignTokens) {
1373
+ return eventuallyResolveDesignTokens(valuesByBreakpoint[breakpointId]);
1727
1374
  }
1728
- // otherwise, save it to the stylesheet
1729
- mediaQueriesTemplate[breakpointId].cssByClassName[className] = toCSSString(stylesForBreakpointWithoutUndefined);
1730
- }
1731
- // all generated classNames are saved in the tree node
1732
- // to be handled by the sdk later
1733
- // each node will get N classNames, where N is the number of breakpoints
1734
- // browsers process classNames in the order they are defined
1735
- // meaning that in case of className1 className2 className3
1736
- // className3 will win over className2 and className1
1737
- // making sure that we respect the order of breakpoints from
1738
- // we can achieve "desktop first" or "mobile first" approach to style over-writes
1739
- if (patternWrapper) {
1740
- currentNode.id = currentNode.id ?? generateRandomId(15);
1741
- // @ts-expect-error -- valueByBreakpoint is not explicitly defined, but it's already defined in the patternWrapper styles
1742
- patternWrapper.variables.cfSsrClassName = {
1743
- ...(patternWrapper.variables.cfSsrClassName ?? {}),
1744
- type: 'DesignValue',
1745
- [currentNode.id]: {
1746
- valuesByBreakpoint: {
1747
- [breakpointIds[0]]: currentNodeClassNames.join(' '),
1748
- },
1749
- },
1750
- };
1375
+ return valuesByBreakpoint[breakpointId];
1751
1376
  }
1752
- else {
1753
- currentNode.variables.cfSsrClassName = {
1754
- type: 'DesignValue',
1755
- valuesByBreakpoint: {
1756
- [breakpointIds[0]]: currentNodeClassNames.join(' '),
1757
- },
1758
- };
1377
+ }
1378
+ // If no breakpoint matched, we search and apply the fallback breakpoint
1379
+ const fallbackBreakpointIndex = getFallbackBreakpointIndex(breakpoints);
1380
+ const fallbackBreakpointId = breakpoints[fallbackBreakpointIndex]?.id;
1381
+ if (isValidBreakpointValue(valuesByBreakpoint[fallbackBreakpointId])) {
1382
+ if (resolveDesignTokens) {
1383
+ return eventuallyResolveDesignTokens(valuesByBreakpoint[fallbackBreakpointId]);
1759
1384
  }
1760
- queue.push(...currentNode.children);
1385
+ return valuesByBreakpoint[fallbackBreakpointId];
1761
1386
  }
1387
+ }
1388
+ else {
1389
+ // Old design properties did not support breakpoints, keep for backward compatibility
1390
+ return valuesByBreakpoint;
1391
+ }
1392
+ };
1393
+
1394
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1395
+ const isLinkToAsset = (variable) => {
1396
+ if (!variable)
1397
+ return false;
1398
+ if (typeof variable !== 'object')
1399
+ return false;
1400
+ return (variable.sys?.linkType === 'Asset' &&
1401
+ typeof variable.sys?.id === 'string' &&
1402
+ !!variable.sys?.id &&
1403
+ variable.sys?.type === 'Link');
1404
+ };
1405
+
1406
+ const isLink = (maybeLink) => {
1407
+ if (maybeLink === null)
1408
+ return false;
1409
+ if (typeof maybeLink !== 'object')
1410
+ return false;
1411
+ const link = maybeLink;
1412
+ return Boolean(link.sys?.id) && link.sys?.type === 'Link';
1413
+ };
1414
+
1415
+ /**
1416
+ * This module encapsulates format of the path to a deep reference.
1417
+ */
1418
+ const parseDataSourcePathIntoFieldset = (path) => {
1419
+ const parsedPath = parseDeepPath(path);
1420
+ if (null === parsedPath) {
1421
+ throw new Error(`Cannot parse path '${path}' as deep path`);
1422
+ }
1423
+ return parsedPath.fields.map((field) => [null, field, '~locale']);
1424
+ };
1425
+ /**
1426
+ * Parse path into components, supports L1 references (one reference follow) atm.
1427
+ * @param path from data source. eg. `/uuid123/fields/image/~locale/fields/file/~locale`
1428
+ * eg. `/uuid123/fields/file/~locale/fields/title/~locale`
1429
+ * @returns
1430
+ */
1431
+ const parseDataSourcePathWithL1DeepBindings = (path) => {
1432
+ const parsedPath = parseDeepPath(path);
1433
+ if (null === parsedPath) {
1434
+ throw new Error(`Cannot parse path '${path}' as deep path`);
1435
+ }
1436
+ return {
1437
+ key: parsedPath.key,
1438
+ field: parsedPath.fields[0],
1439
+ referentField: parsedPath.fields[1],
1440
+ };
1441
+ };
1442
+ /**
1443
+ * Detects if paths is valid deep-path, like:
1444
+ * - /gV6yKXp61hfYrR7rEyKxY/fields/mainStory/~locale/fields/cover/~locale/fields/title/~locale
1445
+ * or regular, like:
1446
+ * - /6J8eA60yXwdm5eyUh9fX6/fields/mainStory/~locale
1447
+ * @returns
1448
+ */
1449
+ const isDeepPath = (deepPathCandidate) => {
1450
+ const deepPathParsed = parseDeepPath(deepPathCandidate);
1451
+ if (!deepPathParsed) {
1452
+ return false;
1453
+ }
1454
+ return deepPathParsed.fields.length > 1;
1455
+ };
1456
+ const parseDeepPath = (deepPathCandidate) => {
1457
+ // ALGORITHM:
1458
+ // We start with deep path in form:
1459
+ // /uuid123/fields/mainStory/~locale/fields/cover/~locale/fields/title/~locale
1460
+ // First turn string into array of segments
1461
+ // ['', 'uuid123', 'fields', 'mainStory', '~locale', 'fields', 'cover', '~locale', 'fields', 'title', '~locale']
1462
+ // Then group segments into intermediate represenatation - chunks, where each non-initial chunk starts with 'fields'
1463
+ // [
1464
+ // [ "", "uuid123" ],
1465
+ // [ "fields", "mainStory", "~locale" ],
1466
+ // [ "fields", "cover", "~locale" ],
1467
+ // [ "fields", "title", "~locale" ]
1468
+ // ]
1469
+ // Then check "initial" chunk for corretness
1470
+ // Then check all "field-leading" chunks for correctness
1471
+ const isValidInitialChunk = (initialChunk) => {
1472
+ // must have start with '' and have at least 2 segments, second non-empty
1473
+ // eg. /-_432uuid123123
1474
+ return /^\/([^/^~]+)$/.test(initialChunk.join('/'));
1475
+ };
1476
+ const isValidFieldChunk = (fieldChunk) => {
1477
+ // must start with 'fields' and have at least 3 segments, second non-empty and last segment must be '~locale'
1478
+ // eg. fields/-32234mainStory/~locale
1479
+ return /^fields\/[^/^~]+\/~locale$/.test(fieldChunk.join('/'));
1762
1480
  };
1763
- iterateOverTreeAndExtractStyles({
1764
- componentTree: experienceTreeRoot,
1765
- dataSource: experience.entityStore?.dataSource ?? {},
1766
- unboundValues: experience.entityStore?.unboundValues ?? {},
1767
- componentSettings: experience.entityStore?.experienceEntryFields?.componentSettings,
1768
- });
1769
- // once the whole tree was traversed, for each breakpoint, I aggregate the styles
1770
- // for each generated className into one css string
1771
- const styleSheet = Object.entries(mediaQueriesTemplate).reduce((acc, [, breakpointPayload]) => {
1772
- return `${acc}${toMediaQuery(breakpointPayload)}`;
1773
- }, '');
1774
- return styleSheet;
1775
- };
1776
- const isCfStyleAttribute = (variableName) => {
1777
- return CF_STYLE_ATTRIBUTES.includes(variableName);
1778
- };
1779
- const maybePopulateDesignTokenValue = (variableName, variableValue, mapOfDesignVariableKeys) => {
1780
- // TODO: refactor to reuse fn from core package
1781
- if (typeof variableValue !== 'string') {
1782
- return variableValue;
1481
+ const deepPathSegments = deepPathCandidate.split('/');
1482
+ const chunks = chunkSegments(deepPathSegments, { startNextChunkOnElementEqualTo: 'fields' });
1483
+ if (chunks.length <= 1) {
1484
+ return null; // malformed path, even regular paths have at least 2 chunks
1783
1485
  }
1784
- if (!isCfStyleAttribute(variableName)) {
1785
- return variableValue;
1486
+ else if (chunks.length === 2) {
1487
+ return null; // deep paths have at least 3 chunks
1786
1488
  }
1787
- const resolveSimpleDesignToken = (variableName, variableValue) => {
1788
- const nonTemplateDesignTokenValue = variableValue.replace(templateStringRegex, '$1');
1789
- const tokenValue = mapOfDesignVariableKeys[nonTemplateDesignTokenValue];
1790
- if (!tokenValue) {
1791
- if (builtInStyles[variableName]) {
1792
- return builtInStyles[variableName].defaultValue;
1793
- }
1794
- if (optionalBuiltInStyles[variableName]) {
1795
- return optionalBuiltInStyles[variableName].defaultValue;
1796
- }
1797
- return '0px';
1489
+ // With 3+ chunks we can now check for deep path correctness
1490
+ const [initialChunk, ...fieldChunks] = chunks;
1491
+ if (!isValidInitialChunk(initialChunk)) {
1492
+ return null;
1493
+ }
1494
+ if (!fieldChunks.every(isValidFieldChunk)) {
1495
+ return null;
1496
+ }
1497
+ return {
1498
+ key: initialChunk[1], // pick uuid from initial chunk ['','uuid123'],
1499
+ fields: fieldChunks.map((fieldChunk) => fieldChunk[1]), // pick only fieldName eg. from ['fields','mainStory', '~locale'] we pick `mainStory`
1500
+ };
1501
+ };
1502
+ const chunkSegments = (segments, { startNextChunkOnElementEqualTo }) => {
1503
+ const chunks = [];
1504
+ let currentChunk = [];
1505
+ const isSegmentBeginningOfChunk = (segment) => segment === startNextChunkOnElementEqualTo;
1506
+ const excludeEmptyChunks = (chunk) => chunk.length > 0;
1507
+ for (let i = 0; i < segments.length; i++) {
1508
+ const isInitialElement = i === 0;
1509
+ const segment = segments[i];
1510
+ if (isInitialElement) {
1511
+ currentChunk = [segment];
1798
1512
  }
1799
- if (variableName === 'cfBorder' || variableName.startsWith('cfBorder_')) {
1800
- if (typeof tokenValue === 'object') {
1801
- const { width, style, color } = tokenValue;
1802
- return `${width} ${style} ${color}`;
1803
- }
1513
+ else if (isSegmentBeginningOfChunk(segment)) {
1514
+ chunks.push(currentChunk);
1515
+ currentChunk = [segment];
1516
+ }
1517
+ else {
1518
+ currentChunk.push(segment);
1804
1519
  }
1805
- return tokenValue;
1806
- };
1807
- const templateStringRegex = /\${(.+?)}/g;
1808
- const parts = variableValue.split(' ');
1809
- let resolvedValue = '';
1810
- for (const part of parts) {
1811
- const tokenValue = templateStringRegex.test(part)
1812
- ? resolveSimpleDesignToken(variableName, part)
1813
- : part;
1814
- resolvedValue += `${tokenValue} `;
1815
1520
  }
1816
- // Not trimming would end up with a trailing space that breaks the check in `calculateNodeDefaultHeight`
1817
- return resolvedValue.trim();
1521
+ chunks.push(currentChunk);
1522
+ return chunks.filter(excludeEmptyChunks);
1818
1523
  };
1819
- const resolveBackgroundImageBinding = ({ variableData, getBoundEntityById, dataSource = {}, unboundValues = {}, componentVariablesOverwrites, componentSettings = { variableDefinitions: {} }, }) => {
1820
- if (variableData.type === 'UnboundValue') {
1821
- const uuid = variableData.key;
1822
- return unboundValues[uuid]?.value;
1524
+ const lastPathNamedSegmentEq = (path, expectedName) => {
1525
+ // `/key123/fields/featureImage/~locale/fields/file/~locale`
1526
+ // ['', 'key123', 'fields', 'featureImage', '~locale', 'fields', 'file', '~locale']
1527
+ const segments = path.split('/');
1528
+ if (segments.length < 2) {
1529
+ 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.`);
1530
+ return false;
1823
1531
  }
1824
- if (variableData.type === 'ComponentValue') {
1825
- const variableDefinitionKey = variableData.key;
1826
- const variableDefinition = componentSettings.variableDefinitions[variableDefinitionKey];
1827
- // @ts-expect-error TODO: Types coming from validations erroneously assume that `defaultValue` can be a primitive value (e.g. string or number)
1828
- const defaultValueKey = variableDefinition.defaultValue?.key;
1829
- const defaultValue = unboundValues[defaultValueKey].value;
1830
- const userSetValue = componentVariablesOverwrites?.[variableDefinitionKey];
1831
- // userSetValue is a ComponentValue we can safely return the default value
1832
- if (!userSetValue || userSetValue.type === 'ComponentValue') {
1833
- return defaultValue;
1532
+ const secondLast = segments[segments.length - 2]; // skipping trailing '~locale'
1533
+ return secondLast === expectedName;
1534
+ };
1535
+
1536
+ const resolveHyperlinkPattern = (pattern, entry, locale) => {
1537
+ if (!entry || !locale)
1538
+ return null;
1539
+ const variables = {
1540
+ entry,
1541
+ locale,
1542
+ };
1543
+ return buildTemplate({ template: pattern, context: variables });
1544
+ };
1545
+ function getValue(obj, path) {
1546
+ return path
1547
+ .replace(/\[/g, '.')
1548
+ .replace(/\]/g, '')
1549
+ .split('.')
1550
+ .reduce((o, k) => (o || {})[k], obj);
1551
+ }
1552
+ function addLocale(str, locale) {
1553
+ const fieldsIndicator = 'fields';
1554
+ const fieldsIndex = str.indexOf(fieldsIndicator);
1555
+ if (fieldsIndex !== -1) {
1556
+ const dotIndex = str.indexOf('.', fieldsIndex + fieldsIndicator.length + 1); // +1 for '.'
1557
+ if (dotIndex !== -1) {
1558
+ return str.slice(0, dotIndex + 1) + locale + '.' + str.slice(dotIndex + 1);
1834
1559
  }
1835
- // at this point userSetValue will either be type of 'DesignValue' or 'BoundValue'
1836
- // so we recursively run resolution again to resolve it
1837
- const resolvedValue = resolveBackgroundImageBinding({
1838
- variableData: userSetValue,
1839
- getBoundEntityById,
1840
- dataSource,
1841
- unboundValues,
1842
- componentVariablesOverwrites,
1843
- componentSettings,
1844
- });
1845
- return resolvedValue || defaultValue;
1846
1560
  }
1847
- if (variableData.type === 'BoundValue') {
1848
- // '/lUERH7tX7nJTaPX6f0udB/fields/assetReference/~locale/fields/file/~locale'
1849
- const [, uuid] = variableData.path.split('/');
1850
- const binding = dataSource[uuid];
1851
- const boundEntity = getBoundEntityById(binding.sys.id);
1852
- if (!boundEntity) {
1853
- return;
1561
+ return str;
1562
+ }
1563
+ function getTemplateValue(ctx, path) {
1564
+ const pathWithLocale = addLocale(path, ctx.locale);
1565
+ const retrievedValue = getValue(ctx, pathWithLocale);
1566
+ return typeof retrievedValue === 'object' && retrievedValue !== null
1567
+ ? retrievedValue[ctx.locale]
1568
+ : retrievedValue;
1569
+ }
1570
+ function buildTemplate({ template, context, }) {
1571
+ const localeVariable = /{\s*locale\s*}/g;
1572
+ // e.g. "{ page.sys.id }"
1573
+ const variables = /{\s*([\S]+?)\s*}/g;
1574
+ return (template
1575
+ // first replace the locale pattern
1576
+ .replace(localeVariable, context.locale)
1577
+ // then resolve the remaining variables
1578
+ .replace(variables, (_, path) => {
1579
+ const fallback = path + '_NOT_FOUND';
1580
+ const value = getTemplateValue(context, path) ?? fallback;
1581
+ // using _.result didn't gave proper results so we run our own version of it
1582
+ return String(typeof value === 'function' ? value() : value);
1583
+ }));
1584
+ }
1585
+
1586
+ const stylesToKeep = ['cfImageAsset'];
1587
+ const stylesToRemove = CF_STYLE_ATTRIBUTES.filter((style) => !stylesToKeep.includes(style));
1588
+ const propsToRemove = ['cfHyperlink', 'cfOpenInNewTab', 'cfSsrClassName'];
1589
+ const sanitizeNodeProps = (nodeProps) => {
1590
+ return omit(nodeProps, stylesToRemove, propsToRemove);
1591
+ };
1592
+
1593
+ class ParseError extends Error {
1594
+ constructor(message) {
1595
+ super(message);
1596
+ }
1597
+ }
1598
+ const isValidJsonObject = (s) => {
1599
+ try {
1600
+ const result = JSON.parse(s);
1601
+ if ('object' !== typeof result) {
1602
+ return false;
1854
1603
  }
1855
- if (boundEntity.sys.type === 'Asset') {
1856
- return boundEntity.fields.file?.url;
1604
+ return true;
1605
+ }
1606
+ catch (e) {
1607
+ return false;
1608
+ }
1609
+ };
1610
+ const doesMismatchMessageSchema = (event) => {
1611
+ try {
1612
+ tryParseMessage(event);
1613
+ return false;
1614
+ }
1615
+ catch (e) {
1616
+ if (e instanceof ParseError) {
1617
+ return e.message;
1857
1618
  }
1858
- else {
1859
- // '/lUERH7tX7nJTaPX6f0udB/fields/assetReference/~locale/fields/file/~locale'
1860
- // becomes
1861
- // '/fields/assetReference/~locale/fields/file/~locale'
1862
- const pathWithoutUUID = variableData.path.split(uuid)[1];
1863
- // '/fields/assetReference/~locale/fields/file/~locale'
1864
- // becomes
1865
- // '/fields/assetReference/'
1866
- const pathToReferencedAsset = pathWithoutUUID.split('~locale')[0];
1867
- // '/fields/assetReference/'
1868
- // becomes
1869
- // '[fields, assetReference]'
1870
- const [, fieldName] = pathToReferencedAsset.substring(1).split('/') ?? undefined;
1871
- const referenceToAsset = boundEntity.fields[fieldName];
1872
- if (!referenceToAsset) {
1873
- return;
1874
- }
1875
- if (referenceToAsset.sys?.linkType === 'Asset') {
1876
- const referencedAsset = getBoundEntityById(referenceToAsset.sys.id);
1877
- if (!referencedAsset) {
1878
- return;
1879
- }
1880
- return referencedAsset.fields.file?.url;
1881
- }
1619
+ throw e;
1620
+ }
1621
+ };
1622
+ const tryParseMessage = (event) => {
1623
+ if (!event.data) {
1624
+ throw new ParseError('Field event.data is missing');
1625
+ }
1626
+ if ('string' !== typeof event.data) {
1627
+ throw new ParseError(`Field event.data must be a string, instead of '${typeof event.data}'`);
1628
+ }
1629
+ if (!isValidJsonObject(event.data)) {
1630
+ throw new ParseError('Field event.data must be a valid JSON object serialized as string');
1631
+ }
1632
+ const eventData = JSON.parse(event.data);
1633
+ if (!eventData.source) {
1634
+ throw new ParseError(`Field eventData.source must be equal to 'composability-app'`);
1635
+ }
1636
+ if ('composability-app' !== eventData.source) {
1637
+ throw new ParseError(`Field eventData.source must be equal to 'composability-app', instead of '${eventData.source}'`);
1638
+ }
1639
+ // check eventData.eventType
1640
+ const supportedEventTypes = Object.values(INCOMING_EVENTS);
1641
+ if (!supportedEventTypes.includes(eventData.eventType)) {
1642
+ // Expected message: This message is handled in the EntityStore to store fetched entities
1643
+ if (eventData.eventType !== PostMessageMethods.REQUESTED_ENTITIES) {
1644
+ throw new ParseError(`Field eventData.eventType must be one of the supported values: [${supportedEventTypes.join(', ')}]`);
1882
1645
  }
1883
1646
  }
1647
+ return eventData;
1884
1648
  };
1885
- const indexByBreakpoint = ({ variables, breakpointIds, getBoundEntityById, unboundValues = {}, dataSource = {}, componentVariablesOverwrites, componentSettings = { variableDefinitions: {} }, }) => {
1886
- const variableValuesByBreakpoints = breakpointIds.reduce((acc, breakpointId) => {
1649
+ const validateExperienceBuilderConfig = ({ locale, mode, }) => {
1650
+ if (mode === StudioCanvasMode.EDITOR || mode === StudioCanvasMode.READ_ONLY) {
1651
+ return;
1652
+ }
1653
+ if (!locale) {
1654
+ throw new Error('Parameter "locale" is required for experience builder initialization outside of editor mode');
1655
+ }
1656
+ };
1657
+
1658
+ const transformVisibility = (value) => {
1659
+ if (value === false) {
1887
1660
  return {
1888
- ...acc,
1889
- [breakpointId]: {},
1661
+ display: 'none',
1890
1662
  };
1891
- }, {});
1892
- const defaultBreakpoint = breakpointIds[0];
1893
- for (const [variableName, variableData] of Object.entries(variables)) {
1894
- // handling the special case - cfBackgroundImageUrl variable, which can be bound or unbound
1895
- // so, we need to resolve it here and pass it down as a css property to be convereted into the CSS
1896
- // I used .startsWith() cause it can be part of a pattern node
1897
- if (variableName === 'cfBackgroundImageUrl' ||
1898
- variableName.startsWith('cfBackgroundImageUrl_')) {
1899
- const imageUrl = resolveBackgroundImageBinding({
1900
- variableData,
1901
- getBoundEntityById,
1902
- unboundValues,
1903
- dataSource,
1904
- componentSettings,
1905
- componentVariablesOverwrites,
1906
- });
1907
- if (imageUrl) {
1908
- variableValuesByBreakpoints[defaultBreakpoint][variableName] = imageUrl;
1909
- }
1910
- continue;
1911
- }
1912
- let resolvedVariableData = variableData;
1913
- if (variableData.type === 'ComponentValue') {
1914
- const variableDefinition = componentSettings?.variableDefinitions[variableData.key];
1915
- if (variableDefinition.group === 'style' && variableDefinition.defaultValue !== undefined) {
1916
- const overrideVariableData = componentVariablesOverwrites?.[variableData.key];
1917
- resolvedVariableData =
1918
- overrideVariableData || variableDefinition.defaultValue;
1919
- }
1920
- }
1921
- if (resolvedVariableData.type !== 'DesignValue') {
1922
- continue;
1663
+ }
1664
+ // Don't explicitly set anything when visible to not overwrite values like `grid` or `flex`.
1665
+ return {};
1666
+ };
1667
+ // Keep this for backwards compatibility - deleting this would be a breaking change
1668
+ // because existing components on a users experience will have the width value as fill
1669
+ // rather than 100%
1670
+ const transformFill = (value) => (value === 'fill' ? '100%' : value);
1671
+ const transformGridColumn = (span) => {
1672
+ if (!span) {
1673
+ return {};
1674
+ }
1675
+ return {
1676
+ gridColumn: `span ${span}`,
1677
+ };
1678
+ };
1679
+ const transformBorderStyle = (value) => {
1680
+ if (!value)
1681
+ return {};
1682
+ const parts = value.split(' ');
1683
+ // Just accept the passed value
1684
+ if (parts.length < 3)
1685
+ return { border: value };
1686
+ const [borderSize, borderStyle, ...borderColorParts] = parts;
1687
+ const borderColor = borderColorParts.join(' ');
1688
+ return {
1689
+ border: `${borderSize} ${borderStyle} ${borderColor}`,
1690
+ };
1691
+ };
1692
+ const transformAlignment = (cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection = 'column') => cfFlexDirection === 'row'
1693
+ ? {
1694
+ alignItems: cfHorizontalAlignment,
1695
+ justifyContent: cfVerticalAlignment === 'center' ? `safe ${cfVerticalAlignment}` : cfVerticalAlignment,
1696
+ }
1697
+ : {
1698
+ alignItems: cfVerticalAlignment,
1699
+ justifyContent: cfHorizontalAlignment === 'center'
1700
+ ? `safe ${cfHorizontalAlignment}`
1701
+ : cfHorizontalAlignment,
1702
+ };
1703
+ const transformBackgroundImage = (cfBackgroundImageUrl, cfBackgroundImageOptions) => {
1704
+ const matchBackgroundSize = (scaling) => {
1705
+ if ('fill' === scaling)
1706
+ return 'cover';
1707
+ if ('fit' === scaling)
1708
+ return 'contain';
1709
+ };
1710
+ const matchBackgroundPosition = (alignment) => {
1711
+ if (!alignment || 'string' !== typeof alignment) {
1712
+ return;
1923
1713
  }
1924
- for (const [breakpointId, variableValue] of Object.entries(resolvedVariableData.valuesByBreakpoint)) {
1925
- if (!isValidBreakpointValue(variableValue)) {
1926
- continue;
1714
+ let [horizontalAlignment, verticalAlignment] = alignment.trim().split(/\s+/, 2);
1715
+ // Special case for handling single values
1716
+ // for backwards compatibility with single values 'right','left', 'center', 'top','bottom'
1717
+ if (horizontalAlignment && !verticalAlignment) {
1718
+ const singleValue = horizontalAlignment;
1719
+ switch (singleValue) {
1720
+ case 'left':
1721
+ horizontalAlignment = 'left';
1722
+ verticalAlignment = 'center';
1723
+ break;
1724
+ case 'right':
1725
+ horizontalAlignment = 'right';
1726
+ verticalAlignment = 'center';
1727
+ break;
1728
+ case 'center':
1729
+ horizontalAlignment = 'center';
1730
+ verticalAlignment = 'center';
1731
+ break;
1732
+ case 'top':
1733
+ horizontalAlignment = 'center';
1734
+ verticalAlignment = 'top';
1735
+ break;
1736
+ case 'bottom':
1737
+ horizontalAlignment = 'center';
1738
+ verticalAlignment = 'bottom';
1739
+ break;
1740
+ // just fall down to the normal validation logic for horiz and vert
1927
1741
  }
1928
- variableValuesByBreakpoints[breakpointId] = {
1929
- ...variableValuesByBreakpoints[breakpointId],
1930
- [variableName]: variableValue,
1931
- };
1932
1742
  }
1743
+ const isHorizontalValid = ['left', 'right', 'center'].includes(horizontalAlignment);
1744
+ const isVerticalValid = ['top', 'bottom', 'center'].includes(verticalAlignment);
1745
+ horizontalAlignment = isHorizontalValid ? horizontalAlignment : 'left';
1746
+ verticalAlignment = isVerticalValid ? verticalAlignment : 'top';
1747
+ return `${horizontalAlignment} ${verticalAlignment}`;
1748
+ };
1749
+ if (!cfBackgroundImageUrl) {
1750
+ return;
1933
1751
  }
1934
- return variableValuesByBreakpoints;
1752
+ let backgroundImage;
1753
+ let backgroundImageSet;
1754
+ if (typeof cfBackgroundImageUrl === 'string') {
1755
+ backgroundImage = `url(${cfBackgroundImageUrl})`;
1756
+ }
1757
+ else {
1758
+ const imgSet = cfBackgroundImageUrl.srcSet?.join(',');
1759
+ backgroundImage = `url(${cfBackgroundImageUrl.url})`;
1760
+ backgroundImageSet = `image-set(${imgSet})`;
1761
+ }
1762
+ return {
1763
+ backgroundImage,
1764
+ backgroundImage2: backgroundImageSet,
1765
+ backgroundRepeat: cfBackgroundImageOptions?.scaling === 'tile' ? 'repeat' : 'no-repeat',
1766
+ backgroundPosition: matchBackgroundPosition(cfBackgroundImageOptions?.alignment),
1767
+ backgroundSize: matchBackgroundSize(cfBackgroundImageOptions?.scaling),
1768
+ };
1935
1769
  };
1936
- /**
1937
- * Flattens the object from
1938
- * {
1939
- * color: {
1940
- * [key]: [value]
1941
- * }
1942
- * }
1943
- *
1944
- * to
1945
- *
1946
- * {
1947
- * 'color.key': [value]
1948
- * }
1949
- */
1950
- const flattenDesignTokenRegistry = (designTokenRegistry) => {
1951
- return Object.entries(designTokenRegistry).reduce((acc, [categoryName, tokenCategory]) => {
1952
- const tokensWithCategory = Object.entries(tokenCategory).reduce((acc, [tokenName, tokenValue]) => {
1953
- return {
1954
- ...acc,
1955
- [`${categoryName}.${tokenName}`]: tokenValue,
1956
- };
1957
- }, {});
1958
- return {
1959
- ...acc,
1960
- ...tokensWithCategory,
1961
- };
1962
- }, {});
1770
+
1771
+ const toCSSAttribute = (key) => {
1772
+ let val = key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
1773
+ // Remove the number from the end of the key to allow for overrides on style properties
1774
+ val = val.replace(/\d+$/, '');
1775
+ return val;
1963
1776
  };
1964
- // Replaces camelCase with kebab-case
1965
- // converts the <key, value> object into a css string
1966
- const toCSSString = (breakpointStyles) => {
1967
- return Object.entries(breakpointStyles)
1968
- .map(([key, value]) => `${key}:${value};`)
1969
- .join('');
1777
+ const buildStyleTag = ({ styles, nodeId }) => {
1778
+ const stylesStr = Object.entries(styles)
1779
+ .filter(([, value]) => value !== undefined)
1780
+ .reduce((acc, [key, value]) => `${acc}
1781
+ ${toCSSAttribute(key)}: ${value};`, '');
1782
+ const className = `cfstyles-${nodeId ? nodeId : md5(stylesStr)}`;
1783
+ const styleRule = `.${className}{ ${stylesStr} }`;
1784
+ return [className, styleRule];
1970
1785
  };
1971
- const toMediaQuery = (breakpointPayload) => {
1972
- const mediaQueryStyles = Object.entries(breakpointPayload.cssByClassName).reduce((acc, [className, css]) => {
1973
- return `${acc}.${className}{${css}}`;
1974
- }, ``);
1975
- if (breakpointPayload.condition === '*') {
1976
- return mediaQueryStyles;
1786
+ const buildCfStyles = ({ cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection, cfFlexReverse, cfFlexWrap, cfMargin, cfPadding, cfBackgroundColor, cfWidth, cfHeight, cfMaxWidth, cfBorder, cfBorderRadius, cfGap, cfBackgroundImageUrl, cfBackgroundImageOptions, cfFontSize, cfFontWeight, cfImageOptions, cfLineHeight, cfLetterSpacing, cfTextColor, cfTextAlign, cfTextTransform, cfTextBold, cfTextItalic, cfTextUnderline, cfColumnSpan, cfVisibility, }) => {
1787
+ return {
1788
+ boxSizing: 'border-box',
1789
+ ...transformVisibility(cfVisibility),
1790
+ margin: cfMargin,
1791
+ padding: cfPadding,
1792
+ backgroundColor: cfBackgroundColor,
1793
+ width: transformFill(cfWidth || cfImageOptions?.width),
1794
+ height: transformFill(cfHeight || cfImageOptions?.height),
1795
+ maxWidth: cfMaxWidth,
1796
+ ...transformGridColumn(cfColumnSpan),
1797
+ ...transformBorderStyle(cfBorder),
1798
+ borderRadius: cfBorderRadius,
1799
+ gap: cfGap,
1800
+ ...transformAlignment(cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection),
1801
+ flexDirection: cfFlexReverse && cfFlexDirection ? `${cfFlexDirection}-reverse` : cfFlexDirection,
1802
+ flexWrap: cfFlexWrap,
1803
+ ...transformBackgroundImage(cfBackgroundImageUrl, cfBackgroundImageOptions),
1804
+ fontSize: cfFontSize,
1805
+ fontWeight: cfTextBold ? 'bold' : cfFontWeight,
1806
+ fontStyle: cfTextItalic ? 'italic' : undefined,
1807
+ textDecoration: cfTextUnderline ? 'underline' : undefined,
1808
+ lineHeight: cfLineHeight,
1809
+ letterSpacing: cfLetterSpacing,
1810
+ color: cfTextColor,
1811
+ textAlign: cfTextAlign,
1812
+ textTransform: cfTextTransform,
1813
+ objectFit: cfImageOptions?.objectFit,
1814
+ objectPosition: cfImageOptions?.objectPosition,
1815
+ };
1816
+ };
1817
+ /**
1818
+ * Container/section default behavior:
1819
+ * Default height => height: EMPTY_CONTAINER_HEIGHT
1820
+ * If a container component has children => height: 'fit-content'
1821
+ */
1822
+ const calculateNodeDefaultHeight = ({ blockId, children, value, }) => {
1823
+ if (!blockId || !isContentfulStructureComponent(blockId) || value !== 'auto') {
1824
+ return value;
1977
1825
  }
1978
- const [evaluation, pixelValue] = [
1979
- breakpointPayload.condition[0],
1980
- breakpointPayload.condition.substring(1),
1981
- ];
1982
- const mediaQueryRule = evaluation === '<' ? 'max-width' : 'min-width';
1983
- return `@media(${mediaQueryRule}:${pixelValue}){${mediaQueryStyles}}`;
1826
+ if (children.length) {
1827
+ return '100%';
1828
+ }
1829
+ return EMPTY_CONTAINER_HEIGHT;
1984
1830
  };
1985
1831
 
1986
1832
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -2142,6 +1988,114 @@ const getOptimizedImageAsset = ({ file, sizes, loading, quality = '100%', format
2142
1988
  return optimizedImageAsset;
2143
1989
  };
2144
1990
 
1991
+ const getDataFromTree = (tree) => {
1992
+ let dataSource = {};
1993
+ let unboundValues = {};
1994
+ const queue = [...tree.root.children];
1995
+ while (queue.length) {
1996
+ const node = queue.shift();
1997
+ if (!node) {
1998
+ continue;
1999
+ }
2000
+ dataSource = { ...dataSource, ...node.data.dataSource };
2001
+ unboundValues = { ...unboundValues, ...node.data.unboundValues };
2002
+ if (node.children.length) {
2003
+ queue.push(...node.children);
2004
+ }
2005
+ }
2006
+ return {
2007
+ dataSource,
2008
+ unboundValues,
2009
+ };
2010
+ };
2011
+ /**
2012
+ * Gets calculates the index to drop the dragged component based on the mouse position
2013
+ * @returns {InsertionData} a object containing a node that will become a parent for dragged component and index at which it must be inserted
2014
+ */
2015
+ const getInsertionData = ({ dropReceiverParentNode, dropReceiverNode, flexDirection, isMouseAtTopBorder, isMouseAtBottomBorder, isMouseInLeftHalf, isMouseInUpperHalf, isOverTopIndicator, isOverBottomIndicator, }) => {
2016
+ const APPEND_INSIDE = dropReceiverNode.children.length;
2017
+ const PREPEND_INSIDE = 0;
2018
+ if (isMouseAtTopBorder || isMouseAtBottomBorder) {
2019
+ const indexOfSectionInParentChildren = dropReceiverParentNode.children.findIndex((n) => n.data.id === dropReceiverNode.data.id);
2020
+ const APPEND_OUTSIDE = indexOfSectionInParentChildren + 1;
2021
+ const PREPEND_OUTSIDE = indexOfSectionInParentChildren;
2022
+ return {
2023
+ // when the mouse is around the border we want to drop the new component as a new section onto the root node
2024
+ node: dropReceiverParentNode,
2025
+ index: isMouseAtBottomBorder ? APPEND_OUTSIDE : PREPEND_OUTSIDE,
2026
+ };
2027
+ }
2028
+ // if over one of the section indicators
2029
+ if (isOverTopIndicator || isOverBottomIndicator) {
2030
+ const indexOfSectionInParentChildren = dropReceiverParentNode.children.findIndex((n) => n.data.id === dropReceiverNode.data.id);
2031
+ const APPEND_OUTSIDE = indexOfSectionInParentChildren + 1;
2032
+ const PREPEND_OUTSIDE = indexOfSectionInParentChildren;
2033
+ return {
2034
+ // when the mouse is around the border we want to drop the new component as a new section onto the root node
2035
+ node: dropReceiverParentNode,
2036
+ index: isOverBottomIndicator ? APPEND_OUTSIDE : PREPEND_OUTSIDE,
2037
+ };
2038
+ }
2039
+ if (flexDirection === undefined || flexDirection === 'row') {
2040
+ return {
2041
+ node: dropReceiverNode,
2042
+ index: isMouseInLeftHalf ? PREPEND_INSIDE : APPEND_INSIDE,
2043
+ };
2044
+ }
2045
+ else {
2046
+ return {
2047
+ node: dropReceiverNode,
2048
+ index: isMouseInUpperHalf ? PREPEND_INSIDE : APPEND_INSIDE,
2049
+ };
2050
+ }
2051
+ };
2052
+ const generateRandomId = (letterCount) => {
2053
+ const LETTERS = 'abcdefghijklmnopqvwxyzABCDEFGHIJKLMNOPQVWXYZ';
2054
+ const NUMS = '0123456789';
2055
+ const ALNUM = NUMS + LETTERS;
2056
+ const times = (n, callback) => Array.from({ length: n }, callback);
2057
+ const random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
2058
+ return times(letterCount, () => ALNUM[random(0, ALNUM.length - 1)]).join('');
2059
+ };
2060
+ const checkIsAssemblyNode = ({ componentId, usedComponents, }) => {
2061
+ if (!usedComponents?.length)
2062
+ return false;
2063
+ return usedComponents.some((usedComponent) => usedComponent.sys.id === componentId);
2064
+ };
2065
+ /** @deprecated use `checkIsAssemblyNode` instead. Will be removed with SDK v5. */
2066
+ const checkIsAssembly = checkIsAssemblyNode;
2067
+ /**
2068
+ * This check assumes that the entry is already ensured to be an experience, i.e. the
2069
+ * content type of the entry is an experience type with the necessary annotations.
2070
+ **/
2071
+ const checkIsAssemblyEntry = (entry) => {
2072
+ return Boolean(entry.fields?.componentSettings);
2073
+ };
2074
+ const checkIsAssemblyDefinition = (component) => component?.category === ASSEMBLY_DEFAULT_CATEGORY;
2075
+ function parseCSSValue(input) {
2076
+ const regex = /^(\d+(\.\d+)?)(px|em|rem)$/;
2077
+ const match = input.match(regex);
2078
+ if (match) {
2079
+ return {
2080
+ value: parseFloat(match[1]),
2081
+ unit: match[3],
2082
+ };
2083
+ }
2084
+ return null;
2085
+ }
2086
+ function getTargetValueInPixels(targetWidthObject) {
2087
+ switch (targetWidthObject.unit) {
2088
+ case 'px':
2089
+ return targetWidthObject.value;
2090
+ case 'em':
2091
+ return targetWidthObject.value * 16;
2092
+ case 'rem':
2093
+ return targetWidthObject.value * 16;
2094
+ default:
2095
+ return targetWidthObject.value;
2096
+ }
2097
+ }
2098
+
2145
2099
  const transformMedia = (asset, variables, resolveDesignValue, variableName, path) => {
2146
2100
  let value;
2147
2101
  // If it is not a deep path and not pointing to the file of the asset,
@@ -2273,462 +2227,508 @@ function getArrayValue(entryOrAsset, path, entityStore) {
2273
2227
  });
2274
2228
  return resolvedEntity;
2275
2229
  }
2276
- else {
2277
- console.warn(`Expected value to be a string or Link, but got: ${JSON.stringify(value)}`);
2278
- return undefined;
2279
- }
2280
- });
2281
- return result;
2282
- }
2283
-
2284
- const transformBoundContentValue = (variables, entityStore, binding, resolveDesignValue, variableName, variableDefinition, path) => {
2285
- const entityOrAsset = entityStore.getEntryOrAsset(binding, path);
2286
- if (!entityOrAsset)
2287
- return;
2288
- switch (variableDefinition.type) {
2289
- case 'Media':
2290
- // If we bound a normal entry field to the media variable we just return the bound value
2291
- if (entityOrAsset.sys.type === 'Entry') {
2292
- return getBoundValue(entityOrAsset, path);
2293
- }
2294
- return transformMedia(entityOrAsset, variables, resolveDesignValue, variableName, path);
2295
- case 'RichText':
2296
- return transformRichText(entityOrAsset, entityStore, path);
2297
- case 'Array':
2298
- return getArrayValue(entityOrAsset, path, entityStore);
2299
- case 'Link':
2300
- return getResolvedEntryFromLink(entityOrAsset, path, entityStore);
2301
- default:
2302
- return getBoundValue(entityOrAsset, path);
2303
- }
2304
- };
2305
-
2306
- const getDataFromTree = (tree) => {
2307
- let dataSource = {};
2308
- let unboundValues = {};
2309
- const queue = [...tree.root.children];
2310
- while (queue.length) {
2311
- const node = queue.shift();
2312
- if (!node) {
2313
- continue;
2314
- }
2315
- dataSource = { ...dataSource, ...node.data.dataSource };
2316
- unboundValues = { ...unboundValues, ...node.data.unboundValues };
2317
- if (node.children.length) {
2318
- queue.push(...node.children);
2319
- }
2320
- }
2321
- return {
2322
- dataSource,
2323
- unboundValues,
2324
- };
2325
- };
2326
- /**
2327
- * Gets calculates the index to drop the dragged component based on the mouse position
2328
- * @returns {InsertionData} a object containing a node that will become a parent for dragged component and index at which it must be inserted
2329
- */
2330
- const getInsertionData = ({ dropReceiverParentNode, dropReceiverNode, flexDirection, isMouseAtTopBorder, isMouseAtBottomBorder, isMouseInLeftHalf, isMouseInUpperHalf, isOverTopIndicator, isOverBottomIndicator, }) => {
2331
- const APPEND_INSIDE = dropReceiverNode.children.length;
2332
- const PREPEND_INSIDE = 0;
2333
- if (isMouseAtTopBorder || isMouseAtBottomBorder) {
2334
- const indexOfSectionInParentChildren = dropReceiverParentNode.children.findIndex((n) => n.data.id === dropReceiverNode.data.id);
2335
- const APPEND_OUTSIDE = indexOfSectionInParentChildren + 1;
2336
- const PREPEND_OUTSIDE = indexOfSectionInParentChildren;
2337
- return {
2338
- // when the mouse is around the border we want to drop the new component as a new section onto the root node
2339
- node: dropReceiverParentNode,
2340
- index: isMouseAtBottomBorder ? APPEND_OUTSIDE : PREPEND_OUTSIDE,
2341
- };
2342
- }
2343
- // if over one of the section indicators
2344
- if (isOverTopIndicator || isOverBottomIndicator) {
2345
- const indexOfSectionInParentChildren = dropReceiverParentNode.children.findIndex((n) => n.data.id === dropReceiverNode.data.id);
2346
- const APPEND_OUTSIDE = indexOfSectionInParentChildren + 1;
2347
- const PREPEND_OUTSIDE = indexOfSectionInParentChildren;
2348
- return {
2349
- // when the mouse is around the border we want to drop the new component as a new section onto the root node
2350
- node: dropReceiverParentNode,
2351
- index: isOverBottomIndicator ? APPEND_OUTSIDE : PREPEND_OUTSIDE,
2352
- };
2353
- }
2354
- if (flexDirection === undefined || flexDirection === 'row') {
2355
- return {
2356
- node: dropReceiverNode,
2357
- index: isMouseInLeftHalf ? PREPEND_INSIDE : APPEND_INSIDE,
2358
- };
2359
- }
2360
- else {
2361
- return {
2362
- node: dropReceiverNode,
2363
- index: isMouseInUpperHalf ? PREPEND_INSIDE : APPEND_INSIDE,
2364
- };
2365
- }
2366
- };
2367
- const generateRandomId = (letterCount) => {
2368
- const LETTERS = 'abcdefghijklmnopqvwxyzABCDEFGHIJKLMNOPQVWXYZ';
2369
- const NUMS = '0123456789';
2370
- const ALNUM = NUMS + LETTERS;
2371
- const times = (n, callback) => Array.from({ length: n }, callback);
2372
- const random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
2373
- return times(letterCount, () => ALNUM[random(0, ALNUM.length - 1)]).join('');
2374
- };
2375
- const checkIsAssemblyNode = ({ componentId, usedComponents, }) => {
2376
- if (!usedComponents?.length)
2377
- return false;
2378
- return usedComponents.some((usedComponent) => usedComponent.sys.id === componentId);
2379
- };
2380
- /** @deprecated use `checkIsAssemblyNode` instead. Will be removed with SDK v5. */
2381
- const checkIsAssembly = checkIsAssemblyNode;
2382
- /**
2383
- * This check assumes that the entry is already ensured to be an experience, i.e. the
2384
- * content type of the entry is an experience type with the necessary annotations.
2385
- **/
2386
- const checkIsAssemblyEntry = (entry) => {
2387
- return Boolean(entry.fields?.componentSettings);
2388
- };
2389
- const checkIsAssemblyDefinition = (component) => component?.category === ASSEMBLY_DEFAULT_CATEGORY;
2390
- function parseCSSValue(input) {
2391
- const regex = /^(\d+(\.\d+)?)(px|em|rem)$/;
2392
- const match = input.match(regex);
2393
- if (match) {
2394
- return {
2395
- value: parseFloat(match[1]),
2396
- unit: match[3],
2397
- };
2398
- }
2399
- return null;
2400
- }
2401
- function getTargetValueInPixels(targetWidthObject) {
2402
- switch (targetWidthObject.unit) {
2403
- case 'px':
2404
- return targetWidthObject.value;
2405
- case 'em':
2406
- return targetWidthObject.value * 16;
2407
- case 'rem':
2408
- return targetWidthObject.value * 16;
2409
- default:
2410
- return targetWidthObject.value;
2411
- }
2230
+ else {
2231
+ console.warn(`Expected value to be a string or Link, but got: ${JSON.stringify(value)}`);
2232
+ return undefined;
2233
+ }
2234
+ });
2235
+ return result;
2412
2236
  }
2413
2237
 
2414
- const isExperienceEntry = (entry) => {
2415
- return (entry?.sys?.type === 'Entry' &&
2416
- !!entry.fields?.title &&
2417
- !!entry.fields?.slug &&
2418
- !!entry.fields?.componentTree &&
2419
- Array.isArray(entry.fields.componentTree.breakpoints) &&
2420
- Array.isArray(entry.fields.componentTree.children) &&
2421
- typeof entry.fields.componentTree.schemaVersion === 'string');
2238
+ const transformBoundContentValue = (variables, entityStore, binding, resolveDesignValue, variableName, variableDefinition, path) => {
2239
+ const entityOrAsset = entityStore.getEntryOrAsset(binding, path);
2240
+ if (!entityOrAsset)
2241
+ return;
2242
+ switch (variableDefinition.type) {
2243
+ case 'Media':
2244
+ // If we bound a normal entry field to the media variable we just return the bound value
2245
+ if (entityOrAsset.sys.type === 'Entry') {
2246
+ return getBoundValue(entityOrAsset, path);
2247
+ }
2248
+ return transformMedia(entityOrAsset, variables, resolveDesignValue, variableName, path);
2249
+ case 'RichText':
2250
+ return transformRichText(entityOrAsset, entityStore, path);
2251
+ case 'Array':
2252
+ return getArrayValue(entityOrAsset, path, entityStore);
2253
+ case 'Link':
2254
+ return getResolvedEntryFromLink(entityOrAsset, path, entityStore);
2255
+ default:
2256
+ return getBoundValue(entityOrAsset, path);
2257
+ }
2422
2258
  };
2423
2259
 
2424
- const MEDIA_QUERY_REGEXP = /(<|>)(\d{1,})(px|cm|mm|in|pt|pc)$/;
2425
- const toCSSMediaQuery = ({ query }) => {
2426
- if (query === '*')
2427
- return undefined;
2428
- const match = query.match(MEDIA_QUERY_REGEXP);
2429
- if (!match)
2430
- return undefined;
2431
- const [, operator, value, unit] = match;
2432
- if (operator === '<') {
2433
- const maxScreenWidth = Number(value) - 1;
2434
- return `(max-width: ${maxScreenWidth}${unit})`;
2435
- }
2436
- else if (operator === '>') {
2437
- const minScreenWidth = Number(value) + 1;
2438
- return `(min-width: ${minScreenWidth}${unit})`;
2260
+ const detachExperienceStyles = (experience) => {
2261
+ const experienceTreeRoot = experience.entityStore?.experienceEntryFields
2262
+ ?.componentTree;
2263
+ if (!experienceTreeRoot) {
2264
+ return;
2439
2265
  }
2440
- return undefined;
2441
- };
2442
- // Remove this helper when upgrading to TypeScript 5.0 - https://github.com/microsoft/TypeScript/issues/48829
2443
- const findLast = (array, predicate) => {
2444
- return array.reverse().find(predicate);
2445
- };
2446
- // Initialise media query matchers. This won't include the always matching fallback breakpoint.
2447
- const mediaQueryMatcher = (breakpoints) => {
2448
- const mediaQueryMatches = {};
2449
- const mediaQueryMatchers = breakpoints
2450
- .map((breakpoint) => {
2451
- const cssMediaQuery = toCSSMediaQuery(breakpoint);
2452
- if (!cssMediaQuery)
2453
- return undefined;
2454
- if (typeof window === 'undefined')
2455
- return undefined;
2456
- const mediaQueryMatcher = window.matchMedia(cssMediaQuery);
2457
- mediaQueryMatches[breakpoint.id] = mediaQueryMatcher.matches;
2458
- return { id: breakpoint.id, signal: mediaQueryMatcher };
2459
- })
2460
- .filter((matcher) => !!matcher);
2461
- return [mediaQueryMatchers, mediaQueryMatches];
2462
- };
2463
- const getActiveBreakpointIndex = (breakpoints, mediaQueryMatches, fallbackBreakpointIndex) => {
2464
- // The breakpoints are ordered (desktop-first: descending by screen width)
2465
- const breakpointsWithMatches = breakpoints.map(({ id }, index) => ({
2466
- id,
2467
- index,
2468
- // The fallback breakpoint with wildcard query will always match
2469
- isMatch: mediaQueryMatches[id] ?? index === fallbackBreakpointIndex,
2470
- }));
2471
- // Find the last breakpoint in the list that matches (desktop-first: the narrowest one)
2472
- const mostSpecificIndex = findLast(breakpointsWithMatches, ({ isMatch }) => isMatch)?.index;
2473
- return mostSpecificIndex ?? fallbackBreakpointIndex;
2474
- };
2475
- const getFallbackBreakpointIndex = (breakpoints) => {
2476
- // We assume that there will be a single breakpoint which uses the wildcard query.
2477
- // If there is none, we just take the first one in the list.
2478
- return Math.max(breakpoints.findIndex(({ query }) => query === '*'), 0);
2479
- };
2480
- const builtInStylesWithDesignTokens = [
2481
- 'cfMargin',
2482
- 'cfPadding',
2483
- 'cfGap',
2484
- 'cfWidth',
2485
- 'cfHeight',
2486
- 'cfBackgroundColor',
2487
- 'cfBorder',
2488
- 'cfBorderRadius',
2489
- 'cfFontSize',
2490
- 'cfLineHeight',
2491
- 'cfLetterSpacing',
2492
- 'cfTextColor',
2493
- 'cfMaxWidth',
2494
- ];
2495
- const isValidBreakpointValue = (value) => {
2496
- return value !== undefined && value !== null && value !== '';
2497
- };
2498
- const getValueForBreakpoint = (valuesByBreakpoint, breakpoints, activeBreakpointIndex, variableName, resolveDesignTokens = true) => {
2499
- const eventuallyResolveDesignTokens = (value) => {
2500
- // For some built-in design properties, we support design tokens
2501
- if (builtInStylesWithDesignTokens.includes(variableName)) {
2502
- return getDesignTokenRegistration(value, variableName);
2266
+ const mapOfDesignVariableKeys = flattenDesignTokenRegistry(designTokensRegistry);
2267
+ // getting breakpoints from the entry componentTree field
2268
+ /**
2269
+ * breakpoints [
2270
+ {
2271
+ id: 'desktop',
2272
+ query: '*',
2273
+ displayName: 'All Sizes',
2274
+ previewSize: '100%'
2275
+ },
2276
+ {
2277
+ id: 'tablet',
2278
+ query: '<992px',
2279
+ displayName: 'Tablet',
2280
+ previewSize: '820px'
2281
+ },
2282
+ {
2283
+ id: 'mobile',
2284
+ query: '<576px',
2285
+ displayName: 'Mobile',
2286
+ previewSize: '390px'
2503
2287
  }
2504
- // For all other properties, we just return the breakpoint-specific value
2505
- return value;
2506
- };
2507
- if (valuesByBreakpoint instanceof Object) {
2508
- // Assume that the values are sorted by media query to apply the cascading CSS logic
2509
- for (let index = activeBreakpointIndex; index >= 0; index--) {
2510
- const breakpointId = breakpoints[index]?.id;
2511
- if (isValidBreakpointValue(valuesByBreakpoint[breakpointId])) {
2512
- // If the value is defined, we use it and stop the breakpoints cascade
2513
- if (resolveDesignTokens) {
2514
- return eventuallyResolveDesignTokens(valuesByBreakpoint[breakpointId]);
2288
+ ]
2289
+ */
2290
+ const { breakpoints } = experienceTreeRoot;
2291
+ // creating the structure which I thought would work best for aggregation
2292
+ const mediaQueriesTemplate = breakpoints.reduce((mediaQueryTemplate, breakpoint) => {
2293
+ return {
2294
+ ...mediaQueryTemplate,
2295
+ [breakpoint.id]: {
2296
+ condition: breakpoint.query,
2297
+ cssByClassName: {},
2298
+ },
2299
+ };
2300
+ }, {});
2301
+ // getting the breakpoint ids
2302
+ const breakpointIds = Object.keys(mediaQueriesTemplate);
2303
+ const iterateOverTreeAndExtractStyles = ({ componentTree, dataSource, unboundValues, componentSettings, componentVariablesOverwrites, patternWrapper, }) => {
2304
+ // traversing the tree
2305
+ const queue = [];
2306
+ queue.push(...componentTree.children);
2307
+ let currentNode = undefined;
2308
+ // for each tree node
2309
+ while (queue.length) {
2310
+ currentNode = queue.shift();
2311
+ if (!currentNode) {
2312
+ break;
2313
+ }
2314
+ const usedComponents = experience.entityStore?.usedComponents ?? [];
2315
+ const isPatternNode = checkIsAssemblyNode({
2316
+ componentId: currentNode.definitionId,
2317
+ usedComponents,
2318
+ });
2319
+ if (isPatternNode) {
2320
+ const patternEntry = usedComponents.find((component) => component.sys.id === currentNode.definitionId);
2321
+ if (!patternEntry || !('fields' in patternEntry)) {
2322
+ continue;
2323
+ }
2324
+ const defaultPatternDivStyles = Object.fromEntries(Object.entries(buildCfStyles({}))
2325
+ .filter(([, value]) => value !== undefined)
2326
+ .map(([key, value]) => [toCSSAttribute(key), value]));
2327
+ // I create a hash of the object above because that would ensure hash stability
2328
+ const styleHash = md5(JSON.stringify(defaultPatternDivStyles));
2329
+ // and prefix the className to make sure the value can be processed
2330
+ const className = `cf-${styleHash}`;
2331
+ for (const breakpointId of breakpointIds) {
2332
+ if (!mediaQueriesTemplate[breakpointId].cssByClassName[className]) {
2333
+ mediaQueriesTemplate[breakpointId].cssByClassName[className] =
2334
+ toCSSString(defaultPatternDivStyles);
2335
+ }
2336
+ }
2337
+ currentNode.variables.cfSsrClassName = {
2338
+ type: 'DesignValue',
2339
+ valuesByBreakpoint: {
2340
+ [breakpointIds[0]]: className,
2341
+ },
2342
+ };
2343
+ // the node of a used pattern contains only the definitionId (id of the patter entry)
2344
+ // as well as the variables overwrites
2345
+ // the layout of a pattern is stored in it's entry
2346
+ iterateOverTreeAndExtractStyles({
2347
+ // that is why we pass it here to iterate of the pattern tree
2348
+ componentTree: patternEntry.fields.componentTree,
2349
+ // but we pass the data source of the experience entry cause that's where the binding is stored
2350
+ dataSource,
2351
+ // unbound values of a pattern store the default values of pattern variables
2352
+ unboundValues: patternEntry.fields.unboundValues,
2353
+ // this is where we can map the pattern variable to it's default value
2354
+ componentSettings: patternEntry.fields.componentSettings,
2355
+ // and this is where the over-writes for the default values are stored
2356
+ // yes, I know, it's a bit confusing
2357
+ componentVariablesOverwrites: currentNode.variables,
2358
+ // pass top-level pattern node to store instance-specific child styles for rendering
2359
+ patternWrapper: currentNode,
2360
+ });
2361
+ continue;
2362
+ }
2363
+ /** Variables value is stored in `valuesByBreakpoint` object
2364
+ * {
2365
+ cfVerticalAlignment: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'center' } },
2366
+ cfHorizontalAlignment: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'center' } },
2367
+ cfMargin: { type: 'DesignValue', valuesByBreakpoint: { desktop: '0 0 0 0' } },
2368
+ cfPadding: { type: 'DesignValue', valuesByBreakpoint: { desktop: '0 0 0 0' } },
2369
+ cfBackgroundColor: {
2370
+ type: 'DesignValue',
2371
+ valuesByBreakpoint: { desktop: 'rgba(246, 246, 246, 1)' }
2372
+ },
2373
+ cfWidth: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'fill' } },
2374
+ cfHeight: {
2375
+ type: 'DesignValue',
2376
+ valuesByBreakpoint: { desktop: 'fit-content' }
2377
+ },
2378
+ cfMaxWidth: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'none' } },
2379
+ cfFlexDirection: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'column' } },
2380
+ cfFlexWrap: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'nowrap' } },
2381
+ cfBorder: {
2382
+ type: 'DesignValue',
2383
+ valuesByBreakpoint: { desktop: '0px solid rgba(0, 0, 0, 0)' }
2384
+ },
2385
+ cfBorderRadius: { type: 'DesignValue', valuesByBreakpoint: { desktop: '0px' } },
2386
+ cfGap: { type: 'DesignValue', valuesByBreakpoint: { desktop: '0px 0px' } },
2387
+ cfHyperlink: { type: 'UnboundValue', key: 'VNc49Qyepd6IzN7rmKUyS' },
2388
+ cfOpenInNewTab: { type: 'UnboundValue', key: 'ZA5YqB2fmREQ4pTKqY5hX' },
2389
+ cfBackgroundImageUrl: { type: 'UnboundValue', key: 'FeskH0WbYD5_RQVXX-1T8' },
2390
+ cfBackgroundImageOptions: { type: 'DesignValue', valuesByBreakpoint: { desktop: [Object] } }
2391
+ }
2392
+ */
2393
+ // so first, I convert it into a map to help me make it easier to access the values
2394
+ const propsByBreakpoint = indexByBreakpoint({
2395
+ variables: currentNode.variables,
2396
+ breakpointIds,
2397
+ unboundValues: unboundValues,
2398
+ dataSource: dataSource,
2399
+ componentSettings,
2400
+ componentVariablesOverwrites,
2401
+ getBoundEntityById: (id) => {
2402
+ return experience.entityStore?.entities.find((entity) => entity.sys.id === id);
2403
+ },
2404
+ });
2405
+ /**
2406
+ * propsByBreakpoint {
2407
+ desktop: {
2408
+ cfVerticalAlignment: 'center',
2409
+ cfHorizontalAlignment: 'center',
2410
+ cfMargin: '0 0 0 0',
2411
+ cfPadding: '0 0 0 0',
2412
+ cfBackgroundColor: 'rgba(246, 246, 246, 1)',
2413
+ cfWidth: 'fill',
2414
+ cfHeight: 'fit-content',
2415
+ cfMaxWidth: 'none',
2416
+ cfFlexDirection: 'column',
2417
+ cfFlexWrap: 'nowrap',
2418
+ cfBorder: '0px solid rgba(0, 0, 0, 0)',
2419
+ cfBorderRadius: '0px',
2420
+ cfGap: '0px 0px',
2421
+ cfBackgroundImageOptions: { scaling: 'fill', alignment: 'left top', targetSize: '2000px' }
2422
+ },
2423
+ tablet: {},
2424
+ mobile: {}
2425
+ }
2426
+ */
2427
+ const currentNodeClassNames = [];
2428
+ // then for each breakpoint
2429
+ for (const breakpointId of breakpointIds) {
2430
+ const propsByBreakpointWithResolvedDesignTokens = Object.entries(propsByBreakpoint[breakpointId]).reduce((acc, [variableName, variableValue]) => {
2431
+ return {
2432
+ ...acc,
2433
+ [variableName]: maybePopulateDesignTokenValue(variableName, variableValue, mapOfDesignVariableKeys),
2434
+ };
2435
+ }, {});
2436
+ // We convert cryptic prop keys to css variables
2437
+ // Eg: cfMargin to margin
2438
+ const stylesForBreakpoint = buildCfStyles(propsByBreakpointWithResolvedDesignTokens);
2439
+ const stylesForBreakpointWithoutUndefined = Object.fromEntries(Object.entries(stylesForBreakpoint)
2440
+ .filter(([, value]) => value !== undefined)
2441
+ .map(([key, value]) => [toCSSAttribute(key), value]));
2442
+ /**
2443
+ * stylesForBreakpoint {
2444
+ margin: '0 0 0 0',
2445
+ padding: '0 0 0 0',
2446
+ 'background-color': 'rgba(246, 246, 246, 1)',
2447
+ width: '100%',
2448
+ height: 'fit-content',
2449
+ 'max-width': 'none',
2450
+ border: '0px solid rgba(0, 0, 0, 0)',
2451
+ 'border-radius': '0px',
2452
+ gap: '0px 0px',
2453
+ 'align-items': 'center',
2454
+ 'justify-content': 'safe center',
2455
+ 'flex-direction': 'column',
2456
+ 'flex-wrap': 'nowrap',
2457
+ 'font-style': 'normal',
2458
+ 'text-decoration': 'none',
2459
+ 'box-sizing': 'border-box'
2515
2460
  }
2516
- return valuesByBreakpoint[breakpointId];
2461
+ */
2462
+ // I create a hash of the object above because that would ensure hash stability
2463
+ const styleHash = md5(JSON.stringify(stylesForBreakpointWithoutUndefined));
2464
+ // and prefix the className to make sure the value can be processed
2465
+ const className = `cf-${styleHash}`;
2466
+ // I save the generated hashes into an array to later save it in the tree node
2467
+ // as cfSsrClassName prop
2468
+ // making sure to avoid the duplicates in case styles for > 1 breakpoints are the same
2469
+ if (!currentNodeClassNames.includes(className)) {
2470
+ currentNodeClassNames.push(className);
2471
+ }
2472
+ // if there is already the similar hash - no need to over-write it
2473
+ if (mediaQueriesTemplate[breakpointId].cssByClassName[className]) {
2474
+ continue;
2475
+ }
2476
+ // otherwise, save it to the stylesheet
2477
+ mediaQueriesTemplate[breakpointId].cssByClassName[className] = toCSSString(stylesForBreakpointWithoutUndefined);
2517
2478
  }
2518
- }
2519
- // If no breakpoint matched, we search and apply the fallback breakpoint
2520
- const fallbackBreakpointIndex = getFallbackBreakpointIndex(breakpoints);
2521
- const fallbackBreakpointId = breakpoints[fallbackBreakpointIndex]?.id;
2522
- if (isValidBreakpointValue(valuesByBreakpoint[fallbackBreakpointId])) {
2523
- if (resolveDesignTokens) {
2524
- return eventuallyResolveDesignTokens(valuesByBreakpoint[fallbackBreakpointId]);
2479
+ // all generated classNames are saved in the tree node
2480
+ // to be handled by the sdk later
2481
+ // each node will get N classNames, where N is the number of breakpoints
2482
+ // browsers process classNames in the order they are defined
2483
+ // meaning that in case of className1 className2 className3
2484
+ // className3 will win over className2 and className1
2485
+ // making sure that we respect the order of breakpoints from
2486
+ // we can achieve "desktop first" or "mobile first" approach to style over-writes
2487
+ if (patternWrapper) {
2488
+ currentNode.id = currentNode.id ?? generateRandomId(15);
2489
+ // @ts-expect-error -- valueByBreakpoint is not explicitly defined, but it's already defined in the patternWrapper styles
2490
+ patternWrapper.variables.cfSsrClassName = {
2491
+ ...(patternWrapper.variables.cfSsrClassName ?? {}),
2492
+ type: 'DesignValue',
2493
+ [currentNode.id]: {
2494
+ valuesByBreakpoint: {
2495
+ [breakpointIds[0]]: currentNodeClassNames.join(' '),
2496
+ },
2497
+ },
2498
+ };
2525
2499
  }
2526
- return valuesByBreakpoint[fallbackBreakpointId];
2500
+ else {
2501
+ currentNode.variables.cfSsrClassName = {
2502
+ type: 'DesignValue',
2503
+ valuesByBreakpoint: {
2504
+ [breakpointIds[0]]: currentNodeClassNames.join(' '),
2505
+ },
2506
+ };
2507
+ }
2508
+ queue.push(...currentNode.children);
2527
2509
  }
2528
- }
2529
- else {
2530
- // Old design properties did not support breakpoints, keep for backward compatibility
2531
- return valuesByBreakpoint;
2532
- }
2533
- };
2534
-
2535
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2536
- const isLinkToAsset = (variable) => {
2537
- if (!variable)
2538
- return false;
2539
- if (typeof variable !== 'object')
2540
- return false;
2541
- return (variable.sys?.linkType === 'Asset' &&
2542
- typeof variable.sys?.id === 'string' &&
2543
- !!variable.sys?.id &&
2544
- variable.sys?.type === 'Link');
2545
- };
2546
-
2547
- const isLink = (maybeLink) => {
2548
- if (maybeLink === null)
2549
- return false;
2550
- if (typeof maybeLink !== 'object')
2551
- return false;
2552
- const link = maybeLink;
2553
- return Boolean(link.sys?.id) && link.sys?.type === 'Link';
2554
- };
2555
-
2556
- /**
2557
- * This module encapsulates format of the path to a deep reference.
2558
- */
2559
- const parseDataSourcePathIntoFieldset = (path) => {
2560
- const parsedPath = parseDeepPath(path);
2561
- if (null === parsedPath) {
2562
- throw new Error(`Cannot parse path '${path}' as deep path`);
2563
- }
2564
- return parsedPath.fields.map((field) => [null, field, '~locale']);
2565
- };
2566
- /**
2567
- * Parse path into components, supports L1 references (one reference follow) atm.
2568
- * @param path from data source. eg. `/uuid123/fields/image/~locale/fields/file/~locale`
2569
- * eg. `/uuid123/fields/file/~locale/fields/title/~locale`
2570
- * @returns
2571
- */
2572
- const parseDataSourcePathWithL1DeepBindings = (path) => {
2573
- const parsedPath = parseDeepPath(path);
2574
- if (null === parsedPath) {
2575
- throw new Error(`Cannot parse path '${path}' as deep path`);
2576
- }
2577
- return {
2578
- key: parsedPath.key,
2579
- field: parsedPath.fields[0],
2580
- referentField: parsedPath.fields[1],
2581
2510
  };
2511
+ iterateOverTreeAndExtractStyles({
2512
+ componentTree: experienceTreeRoot,
2513
+ dataSource: experience.entityStore?.dataSource ?? {},
2514
+ unboundValues: experience.entityStore?.unboundValues ?? {},
2515
+ componentSettings: experience.entityStore?.experienceEntryFields?.componentSettings,
2516
+ });
2517
+ // once the whole tree was traversed, for each breakpoint, I aggregate the styles
2518
+ // for each generated className into one css string
2519
+ const styleSheet = Object.entries(mediaQueriesTemplate).reduce((acc, [, breakpointPayload]) => {
2520
+ return `${acc}${toMediaQuery(breakpointPayload)}`;
2521
+ }, '');
2522
+ return styleSheet;
2582
2523
  };
2583
- /**
2584
- * Detects if paths is valid deep-path, like:
2585
- * - /gV6yKXp61hfYrR7rEyKxY/fields/mainStory/~locale/fields/cover/~locale/fields/title/~locale
2586
- * or regular, like:
2587
- * - /6J8eA60yXwdm5eyUh9fX6/fields/mainStory/~locale
2588
- * @returns
2589
- */
2590
- const isDeepPath = (deepPathCandidate) => {
2591
- const deepPathParsed = parseDeepPath(deepPathCandidate);
2592
- if (!deepPathParsed) {
2593
- return false;
2594
- }
2595
- return deepPathParsed.fields.length > 1;
2524
+ const isCfStyleAttribute = (variableName) => {
2525
+ return CF_STYLE_ATTRIBUTES.includes(variableName);
2596
2526
  };
2597
- const parseDeepPath = (deepPathCandidate) => {
2598
- // ALGORITHM:
2599
- // We start with deep path in form:
2600
- // /uuid123/fields/mainStory/~locale/fields/cover/~locale/fields/title/~locale
2601
- // First turn string into array of segments
2602
- // ['', 'uuid123', 'fields', 'mainStory', '~locale', 'fields', 'cover', '~locale', 'fields', 'title', '~locale']
2603
- // Then group segments into intermediate represenatation - chunks, where each non-initial chunk starts with 'fields'
2604
- // [
2605
- // [ "", "uuid123" ],
2606
- // [ "fields", "mainStory", "~locale" ],
2607
- // [ "fields", "cover", "~locale" ],
2608
- // [ "fields", "title", "~locale" ]
2609
- // ]
2610
- // Then check "initial" chunk for corretness
2611
- // Then check all "field-leading" chunks for correctness
2612
- const isValidInitialChunk = (initialChunk) => {
2613
- // must have start with '' and have at least 2 segments, second non-empty
2614
- // eg. /-_432uuid123123
2615
- return /^\/([^/^~]+)$/.test(initialChunk.join('/'));
2616
- };
2617
- const isValidFieldChunk = (fieldChunk) => {
2618
- // must start with 'fields' and have at least 3 segments, second non-empty and last segment must be '~locale'
2619
- // eg. fields/-32234mainStory/~locale
2620
- return /^fields\/[^/^~]+\/~locale$/.test(fieldChunk.join('/'));
2621
- };
2622
- const deepPathSegments = deepPathCandidate.split('/');
2623
- const chunks = chunkSegments(deepPathSegments, { startNextChunkOnElementEqualTo: 'fields' });
2624
- if (chunks.length <= 1) {
2625
- return null; // malformed path, even regular paths have at least 2 chunks
2527
+ const maybePopulateDesignTokenValue = (variableName, variableValue, mapOfDesignVariableKeys) => {
2528
+ // TODO: refactor to reuse fn from core package
2529
+ if (typeof variableValue !== 'string') {
2530
+ return variableValue;
2626
2531
  }
2627
- else if (chunks.length === 2) {
2628
- return null; // deep paths have at least 3 chunks
2532
+ if (!isCfStyleAttribute(variableName)) {
2533
+ return variableValue;
2534
+ }
2535
+ const resolveSimpleDesignToken = (variableName, variableValue) => {
2536
+ const nonTemplateDesignTokenValue = variableValue.replace(templateStringRegex, '$1');
2537
+ const tokenValue = mapOfDesignVariableKeys[nonTemplateDesignTokenValue];
2538
+ if (!tokenValue) {
2539
+ if (builtInStyles[variableName]) {
2540
+ return builtInStyles[variableName].defaultValue;
2541
+ }
2542
+ if (optionalBuiltInStyles[variableName]) {
2543
+ return optionalBuiltInStyles[variableName].defaultValue;
2544
+ }
2545
+ return '0px';
2546
+ }
2547
+ if (variableName === 'cfBorder' || variableName.startsWith('cfBorder_')) {
2548
+ if (typeof tokenValue === 'object') {
2549
+ const { width, style, color } = tokenValue;
2550
+ return `${width} ${style} ${color}`;
2551
+ }
2552
+ }
2553
+ return tokenValue;
2554
+ };
2555
+ const templateStringRegex = /\${(.+?)}/g;
2556
+ const parts = variableValue.split(' ');
2557
+ let resolvedValue = '';
2558
+ for (const part of parts) {
2559
+ const tokenValue = templateStringRegex.test(part)
2560
+ ? resolveSimpleDesignToken(variableName, part)
2561
+ : part;
2562
+ resolvedValue += `${tokenValue} `;
2629
2563
  }
2630
- // With 3+ chunks we can now check for deep path correctness
2631
- const [initialChunk, ...fieldChunks] = chunks;
2632
- if (!isValidInitialChunk(initialChunk)) {
2633
- return null;
2564
+ // Not trimming would end up with a trailing space that breaks the check in `calculateNodeDefaultHeight`
2565
+ return resolvedValue.trim();
2566
+ };
2567
+ const resolveBackgroundImageBinding = ({ variableData, getBoundEntityById, dataSource = {}, unboundValues = {}, componentVariablesOverwrites, componentSettings = { variableDefinitions: {} }, }) => {
2568
+ if (variableData.type === 'UnboundValue') {
2569
+ const uuid = variableData.key;
2570
+ return unboundValues[uuid]?.value;
2634
2571
  }
2635
- if (!fieldChunks.every(isValidFieldChunk)) {
2636
- return null;
2572
+ if (variableData.type === 'ComponentValue') {
2573
+ const variableDefinitionKey = variableData.key;
2574
+ const variableDefinition = componentSettings.variableDefinitions[variableDefinitionKey];
2575
+ // @ts-expect-error TODO: Types coming from validations erroneously assume that `defaultValue` can be a primitive value (e.g. string or number)
2576
+ const defaultValueKey = variableDefinition.defaultValue?.key;
2577
+ const defaultValue = unboundValues[defaultValueKey].value;
2578
+ const userSetValue = componentVariablesOverwrites?.[variableDefinitionKey];
2579
+ // userSetValue is a ComponentValue we can safely return the default value
2580
+ if (!userSetValue || userSetValue.type === 'ComponentValue') {
2581
+ return defaultValue;
2582
+ }
2583
+ // at this point userSetValue will either be type of 'DesignValue' or 'BoundValue'
2584
+ // so we recursively run resolution again to resolve it
2585
+ const resolvedValue = resolveBackgroundImageBinding({
2586
+ variableData: userSetValue,
2587
+ getBoundEntityById,
2588
+ dataSource,
2589
+ unboundValues,
2590
+ componentVariablesOverwrites,
2591
+ componentSettings,
2592
+ });
2593
+ return resolvedValue || defaultValue;
2637
2594
  }
2638
- return {
2639
- key: initialChunk[1], // pick uuid from initial chunk ['','uuid123'],
2640
- fields: fieldChunks.map((fieldChunk) => fieldChunk[1]), // pick only fieldName eg. from ['fields','mainStory', '~locale'] we pick `mainStory`
2641
- };
2642
- };
2643
- const chunkSegments = (segments, { startNextChunkOnElementEqualTo }) => {
2644
- const chunks = [];
2645
- let currentChunk = [];
2646
- const isSegmentBeginningOfChunk = (segment) => segment === startNextChunkOnElementEqualTo;
2647
- const excludeEmptyChunks = (chunk) => chunk.length > 0;
2648
- for (let i = 0; i < segments.length; i++) {
2649
- const isInitialElement = i === 0;
2650
- const segment = segments[i];
2651
- if (isInitialElement) {
2652
- currentChunk = [segment];
2595
+ if (variableData.type === 'BoundValue') {
2596
+ // '/lUERH7tX7nJTaPX6f0udB/fields/assetReference/~locale/fields/file/~locale'
2597
+ const [, uuid] = variableData.path.split('/');
2598
+ const binding = dataSource[uuid];
2599
+ const boundEntity = getBoundEntityById(binding.sys.id);
2600
+ if (!boundEntity) {
2601
+ return;
2653
2602
  }
2654
- else if (isSegmentBeginningOfChunk(segment)) {
2655
- chunks.push(currentChunk);
2656
- currentChunk = [segment];
2603
+ if (boundEntity.sys.type === 'Asset') {
2604
+ return boundEntity.fields.file?.url;
2657
2605
  }
2658
2606
  else {
2659
- currentChunk.push(segment);
2607
+ // '/lUERH7tX7nJTaPX6f0udB/fields/assetReference/~locale/fields/file/~locale'
2608
+ // becomes
2609
+ // '/fields/assetReference/~locale/fields/file/~locale'
2610
+ const pathWithoutUUID = variableData.path.split(uuid)[1];
2611
+ // '/fields/assetReference/~locale/fields/file/~locale'
2612
+ // becomes
2613
+ // '/fields/assetReference/'
2614
+ const pathToReferencedAsset = pathWithoutUUID.split('~locale')[0];
2615
+ // '/fields/assetReference/'
2616
+ // becomes
2617
+ // '[fields, assetReference]'
2618
+ const [, fieldName] = pathToReferencedAsset.substring(1).split('/') ?? undefined;
2619
+ const referenceToAsset = boundEntity.fields[fieldName];
2620
+ if (!referenceToAsset) {
2621
+ return;
2622
+ }
2623
+ if (referenceToAsset.sys?.linkType === 'Asset') {
2624
+ const referencedAsset = getBoundEntityById(referenceToAsset.sys.id);
2625
+ if (!referencedAsset) {
2626
+ return;
2627
+ }
2628
+ return referencedAsset.fields.file?.url;
2629
+ }
2660
2630
  }
2661
2631
  }
2662
- chunks.push(currentChunk);
2663
- return chunks.filter(excludeEmptyChunks);
2664
2632
  };
2665
- const lastPathNamedSegmentEq = (path, expectedName) => {
2666
- // `/key123/fields/featureImage/~locale/fields/file/~locale`
2667
- // ['', 'key123', 'fields', 'featureImage', '~locale', 'fields', 'file', '~locale']
2668
- const segments = path.split('/');
2669
- if (segments.length < 2) {
2670
- 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.`);
2671
- return false;
2633
+ const indexByBreakpoint = ({ variables, breakpointIds, getBoundEntityById, unboundValues = {}, dataSource = {}, componentVariablesOverwrites, componentSettings = { variableDefinitions: {} }, }) => {
2634
+ const variableValuesByBreakpoints = breakpointIds.reduce((acc, breakpointId) => {
2635
+ return {
2636
+ ...acc,
2637
+ [breakpointId]: {},
2638
+ };
2639
+ }, {});
2640
+ const defaultBreakpoint = breakpointIds[0];
2641
+ for (const [variableName, variableData] of Object.entries(variables)) {
2642
+ // handling the special case - cfBackgroundImageUrl variable, which can be bound or unbound
2643
+ // so, we need to resolve it here and pass it down as a css property to be convereted into the CSS
2644
+ // I used .startsWith() cause it can be part of a pattern node
2645
+ if (variableName === 'cfBackgroundImageUrl' ||
2646
+ variableName.startsWith('cfBackgroundImageUrl_')) {
2647
+ const imageUrl = resolveBackgroundImageBinding({
2648
+ variableData,
2649
+ getBoundEntityById,
2650
+ unboundValues,
2651
+ dataSource,
2652
+ componentSettings,
2653
+ componentVariablesOverwrites,
2654
+ });
2655
+ if (imageUrl) {
2656
+ variableValuesByBreakpoints[defaultBreakpoint][variableName] = imageUrl;
2657
+ }
2658
+ continue;
2659
+ }
2660
+ let resolvedVariableData = variableData;
2661
+ if (variableData.type === 'ComponentValue') {
2662
+ const variableDefinition = componentSettings?.variableDefinitions[variableData.key];
2663
+ if (variableDefinition.group === 'style' && variableDefinition.defaultValue !== undefined) {
2664
+ const overrideVariableData = componentVariablesOverwrites?.[variableData.key];
2665
+ resolvedVariableData =
2666
+ overrideVariableData || variableDefinition.defaultValue;
2667
+ }
2668
+ }
2669
+ if (resolvedVariableData.type !== 'DesignValue') {
2670
+ continue;
2671
+ }
2672
+ for (const [breakpointId, variableValue] of Object.entries(resolvedVariableData.valuesByBreakpoint)) {
2673
+ if (!isValidBreakpointValue(variableValue)) {
2674
+ continue;
2675
+ }
2676
+ variableValuesByBreakpoints[breakpointId] = {
2677
+ ...variableValuesByBreakpoints[breakpointId],
2678
+ [variableName]: variableValue,
2679
+ };
2680
+ }
2672
2681
  }
2673
- const secondLast = segments[segments.length - 2]; // skipping trailing '~locale'
2674
- return secondLast === expectedName;
2682
+ return variableValuesByBreakpoints;
2675
2683
  };
2676
-
2677
- const resolveHyperlinkPattern = (pattern, entry, locale) => {
2678
- if (!entry || !locale)
2679
- return null;
2680
- const variables = {
2681
- entry,
2682
- locale,
2683
- };
2684
- return buildTemplate({ template: pattern, context: variables });
2684
+ /**
2685
+ * Flattens the object from
2686
+ * {
2687
+ * color: {
2688
+ * [key]: [value]
2689
+ * }
2690
+ * }
2691
+ *
2692
+ * to
2693
+ *
2694
+ * {
2695
+ * 'color.key': [value]
2696
+ * }
2697
+ */
2698
+ const flattenDesignTokenRegistry = (designTokenRegistry) => {
2699
+ return Object.entries(designTokenRegistry).reduce((acc, [categoryName, tokenCategory]) => {
2700
+ const tokensWithCategory = Object.entries(tokenCategory).reduce((acc, [tokenName, tokenValue]) => {
2701
+ return {
2702
+ ...acc,
2703
+ [`${categoryName}.${tokenName}`]: tokenValue,
2704
+ };
2705
+ }, {});
2706
+ return {
2707
+ ...acc,
2708
+ ...tokensWithCategory,
2709
+ };
2710
+ }, {});
2685
2711
  };
2686
- function getValue(obj, path) {
2687
- return path
2688
- .replace(/\[/g, '.')
2689
- .replace(/\]/g, '')
2690
- .split('.')
2691
- .reduce((o, k) => (o || {})[k], obj);
2692
- }
2693
- function addLocale(str, locale) {
2694
- const fieldsIndicator = 'fields';
2695
- const fieldsIndex = str.indexOf(fieldsIndicator);
2696
- if (fieldsIndex !== -1) {
2697
- const dotIndex = str.indexOf('.', fieldsIndex + fieldsIndicator.length + 1); // +1 for '.'
2698
- if (dotIndex !== -1) {
2699
- return str.slice(0, dotIndex + 1) + locale + '.' + str.slice(dotIndex + 1);
2700
- }
2712
+ // Replaces camelCase with kebab-case
2713
+ // converts the <key, value> object into a css string
2714
+ const toCSSString = (breakpointStyles) => {
2715
+ return Object.entries(breakpointStyles)
2716
+ .map(([key, value]) => `${key}:${value};`)
2717
+ .join('');
2718
+ };
2719
+ const toMediaQuery = (breakpointPayload) => {
2720
+ const mediaQueryStyles = Object.entries(breakpointPayload.cssByClassName).reduce((acc, [className, css]) => {
2721
+ return `${acc}.${className}{${css}}`;
2722
+ }, ``);
2723
+ if (breakpointPayload.condition === '*') {
2724
+ return mediaQueryStyles;
2701
2725
  }
2702
- return str;
2703
- }
2704
- function getTemplateValue(ctx, path) {
2705
- const pathWithLocale = addLocale(path, ctx.locale);
2706
- const retrievedValue = getValue(ctx, pathWithLocale);
2707
- return typeof retrievedValue === 'object' && retrievedValue !== null
2708
- ? retrievedValue[ctx.locale]
2709
- : retrievedValue;
2710
- }
2711
- function buildTemplate({ template, context, }) {
2712
- const localeVariable = /{\s*locale\s*}/g;
2713
- // e.g. "{ page.sys.id }"
2714
- const variables = /{\s*([\S]+?)\s*}/g;
2715
- return (template
2716
- // first replace the locale pattern
2717
- .replace(localeVariable, context.locale)
2718
- // then resolve the remaining variables
2719
- .replace(variables, (_, path) => {
2720
- const fallback = path + '_NOT_FOUND';
2721
- const value = getTemplateValue(context, path) ?? fallback;
2722
- // using _.result didn't gave proper results so we run our own version of it
2723
- return String(typeof value === 'function' ? value() : value);
2724
- }));
2725
- }
2726
-
2727
- const stylesToKeep = ['cfImageAsset'];
2728
- const stylesToRemove = CF_STYLE_ATTRIBUTES.filter((style) => !stylesToKeep.includes(style));
2729
- const propsToRemove = ['cfHyperlink', 'cfOpenInNewTab', 'cfSsrClassName'];
2730
- const sanitizeNodeProps = (nodeProps) => {
2731
- return omit(nodeProps, stylesToRemove, propsToRemove);
2726
+ const [evaluation, pixelValue] = [
2727
+ breakpointPayload.condition[0],
2728
+ breakpointPayload.condition.substring(1),
2729
+ ];
2730
+ const mediaQueryRule = evaluation === '<' ? 'max-width' : 'min-width';
2731
+ return `@media(${mediaQueryRule}:${pixelValue}){${mediaQueryStyles}}`;
2732
2732
  };
2733
2733
 
2734
2734
  const sendMessage = (eventType, data) => {
@@ -3228,6 +3228,8 @@ var VisualEditorMode;
3228
3228
  VisualEditorMode["InjectScript"] = "injectScript";
3229
3229
  })(VisualEditorMode || (VisualEditorMode = {}));
3230
3230
 
3231
+ // export function createExperience(args: createExperienceArgs): Experience<EntityStore>;
3232
+ // export function createExperience(options: string | createExperienceArgs): Experience<EntityStore> {
3231
3233
  function createExperience(options) {
3232
3234
  if (typeof options === 'string') {
3233
3235
  const entityStore = new EntityStore(options);