@wordpress/global-styles-engine 1.0.1-next.36001005c.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.
Files changed (148) hide show
  1. package/LICENSE.md +788 -0
  2. package/README.md +133 -0
  3. package/build/core/merge.js +61 -0
  4. package/build/core/merge.js.map +7 -0
  5. package/build/core/render.js +997 -0
  6. package/build/core/render.js.map +7 -0
  7. package/build/core/selectors.js +82 -0
  8. package/build/core/selectors.js.map +7 -0
  9. package/build/index.js +77 -0
  10. package/build/index.js.map +7 -0
  11. package/build/settings/get-palette.js +163 -0
  12. package/build/settings/get-palette.js.map +7 -0
  13. package/build/settings/get-setting.js +112 -0
  14. package/build/settings/get-setting.js.map +7 -0
  15. package/build/settings/get-style.js +40 -0
  16. package/build/settings/get-style.js.map +7 -0
  17. package/build/settings/set-setting.js +39 -0
  18. package/build/settings/set-setting.js.map +7 -0
  19. package/build/settings/set-style.js +38 -0
  20. package/build/settings/set-style.js.map +7 -0
  21. package/build/types.js +17 -0
  22. package/build/types.js.map +7 -0
  23. package/build/utils/background.js +53 -0
  24. package/build/utils/background.js.map +7 -0
  25. package/build/utils/common.js +418 -0
  26. package/build/utils/common.js.map +7 -0
  27. package/build/utils/duotone.js +90 -0
  28. package/build/utils/duotone.js.map +7 -0
  29. package/build/utils/fluid.js +173 -0
  30. package/build/utils/fluid.js.map +7 -0
  31. package/build/utils/gap.js +50 -0
  32. package/build/utils/gap.js.map +7 -0
  33. package/build/utils/layout.js +199 -0
  34. package/build/utils/layout.js.map +7 -0
  35. package/build/utils/object.js +50 -0
  36. package/build/utils/object.js.map +7 -0
  37. package/build/utils/spacing.js +38 -0
  38. package/build/utils/spacing.js.map +7 -0
  39. package/build/utils/string.js +31 -0
  40. package/build/utils/string.js.map +7 -0
  41. package/build/utils/typography.js +72 -0
  42. package/build/utils/typography.js.map +7 -0
  43. package/build-module/core/merge.js +27 -0
  44. package/build-module/core/merge.js.map +7 -0
  45. package/build-module/core/render.js +979 -0
  46. package/build-module/core/render.js.map +7 -0
  47. package/build-module/core/selectors.js +58 -0
  48. package/build-module/core/selectors.js.map +7 -0
  49. package/build-module/index.js +37 -0
  50. package/build-module/index.js.map +7 -0
  51. package/build-module/settings/get-palette.js +143 -0
  52. package/build-module/settings/get-palette.js.map +7 -0
  53. package/build-module/settings/get-setting.js +88 -0
  54. package/build-module/settings/get-setting.js.map +7 -0
  55. package/build-module/settings/get-style.js +16 -0
  56. package/build-module/settings/get-style.js.map +7 -0
  57. package/build-module/settings/set-setting.js +15 -0
  58. package/build-module/settings/set-setting.js.map +7 -0
  59. package/build-module/settings/set-style.js +14 -0
  60. package/build-module/settings/set-style.js.map +7 -0
  61. package/build-module/types.js +1 -0
  62. package/build-module/types.js.map +7 -0
  63. package/build-module/utils/background.js +28 -0
  64. package/build-module/utils/background.js.map +7 -0
  65. package/build-module/utils/common.js +382 -0
  66. package/build-module/utils/common.js.map +7 -0
  67. package/build-module/utils/duotone.js +63 -0
  68. package/build-module/utils/duotone.js.map +7 -0
  69. package/build-module/utils/fluid.js +147 -0
  70. package/build-module/utils/fluid.js.map +7 -0
  71. package/build-module/utils/gap.js +25 -0
  72. package/build-module/utils/gap.js.map +7 -0
  73. package/build-module/utils/layout.js +175 -0
  74. package/build-module/utils/layout.js.map +7 -0
  75. package/build-module/utils/object.js +25 -0
  76. package/build-module/utils/object.js.map +7 -0
  77. package/build-module/utils/spacing.js +14 -0
  78. package/build-module/utils/spacing.js.map +7 -0
  79. package/build-module/utils/string.js +7 -0
  80. package/build-module/utils/string.js.map +7 -0
  81. package/build-module/utils/typography.js +50 -0
  82. package/build-module/utils/typography.js.map +7 -0
  83. package/build-types/core/merge.d.ts +13 -0
  84. package/build-types/core/merge.d.ts.map +1 -0
  85. package/build-types/core/render.d.ts +84 -0
  86. package/build-types/core/render.d.ts.map +1 -0
  87. package/build-types/core/selectors.d.ts +19 -0
  88. package/build-types/core/selectors.d.ts.map +1 -0
  89. package/build-types/index.d.ts +13 -0
  90. package/build-types/index.d.ts.map +1 -0
  91. package/build-types/settings/get-palette.d.ts +13 -0
  92. package/build-types/settings/get-palette.d.ts.map +1 -0
  93. package/build-types/settings/get-setting.d.ts +3 -0
  94. package/build-types/settings/get-setting.d.ts.map +1 -0
  95. package/build-types/settings/get-style.d.ts +3 -0
  96. package/build-types/settings/get-style.d.ts.map +1 -0
  97. package/build-types/settings/set-setting.d.ts +3 -0
  98. package/build-types/settings/set-setting.d.ts.map +1 -0
  99. package/build-types/settings/set-style.d.ts +3 -0
  100. package/build-types/settings/set-style.d.ts.map +1 -0
  101. package/build-types/types.d.ts +333 -0
  102. package/build-types/types.d.ts.map +1 -0
  103. package/build-types/utils/background.d.ts +16 -0
  104. package/build-types/utils/background.d.ts.map +1 -0
  105. package/build-types/utils/common.d.ts +169 -0
  106. package/build-types/utils/common.d.ts.map +1 -0
  107. package/build-types/utils/duotone.d.ts +40 -0
  108. package/build-types/utils/duotone.d.ts.map +1 -0
  109. package/build-types/utils/fluid.d.ts +68 -0
  110. package/build-types/utils/fluid.d.ts.map +1 -0
  111. package/build-types/utils/gap.d.ts +27 -0
  112. package/build-types/utils/gap.d.ts.map +1 -0
  113. package/build-types/utils/layout.d.ts +156 -0
  114. package/build-types/utils/layout.d.ts.map +1 -0
  115. package/build-types/utils/object.d.ts +27 -0
  116. package/build-types/utils/object.d.ts.map +1 -0
  117. package/build-types/utils/spacing.d.ts +2 -0
  118. package/build-types/utils/spacing.d.ts.map +1 -0
  119. package/build-types/utils/string.d.ts +9 -0
  120. package/build-types/utils/string.d.ts.map +1 -0
  121. package/build-types/utils/typography.d.ts +28 -0
  122. package/build-types/utils/typography.d.ts.map +1 -0
  123. package/package.json +50 -0
  124. package/src/core/merge.ts +43 -0
  125. package/src/core/render.tsx +1696 -0
  126. package/src/core/selectors.ts +121 -0
  127. package/src/index.ts +29 -0
  128. package/src/settings/get-palette.ts +187 -0
  129. package/src/settings/get-setting.ts +99 -0
  130. package/src/settings/get-style.ts +29 -0
  131. package/src/settings/set-setting.ts +22 -0
  132. package/src/settings/set-style.ts +23 -0
  133. package/src/test/render.test.ts +792 -0
  134. package/src/test/typography-utils.test.ts +354 -0
  135. package/src/test/utils.test.ts +451 -0
  136. package/src/types.ts +408 -0
  137. package/src/utils/background.ts +39 -0
  138. package/src/utils/common.ts +671 -0
  139. package/src/utils/duotone.ts +95 -0
  140. package/src/utils/fluid.ts +311 -0
  141. package/src/utils/gap.ts +56 -0
  142. package/src/utils/layout.ts +174 -0
  143. package/src/utils/object.ts +64 -0
  144. package/src/utils/spacing.ts +13 -0
  145. package/src/utils/string.ts +15 -0
  146. package/src/utils/typography.ts +142 -0
  147. package/tsconfig.json +18 -0
  148. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,1696 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import {
5
+ __EXPERIMENTAL_STYLE_PROPERTY as STYLE_PROPERTY,
6
+ __EXPERIMENTAL_ELEMENTS as ELEMENTS,
7
+ getBlockSupport,
8
+ getBlockTypes,
9
+ store as blocksStore,
10
+ // @ts-expect-error - @wordpress/blocks module doesn't have TypeScript declarations
11
+ } from '@wordpress/blocks';
12
+ import { getCSSRules, getCSSValueFromRawStyle } from '@wordpress/style-engine';
13
+ import { select } from '@wordpress/data';
14
+
15
+ /**
16
+ * Internal dependencies
17
+ */
18
+ import {
19
+ PRESET_METADATA,
20
+ ROOT_BLOCK_SELECTOR,
21
+ ROOT_CSS_PROPERTIES_SELECTOR,
22
+ scopeSelector,
23
+ scopeFeatureSelectors,
24
+ appendToSelector,
25
+ getBlockStyleVariationSelector,
26
+ getResolvedValue,
27
+ } from '../utils/common';
28
+ import { getBlockSelector } from './selectors';
29
+ import { getTypographyFontSizeValue } from '../utils/typography';
30
+ import { getDuotoneFilter } from '../utils/duotone';
31
+ import { kebabCase } from '../utils/string';
32
+ import { getGapCSSValue } from '../utils/gap';
33
+ import { setBackgroundStyleDefaults } from '../utils/background';
34
+ import { LAYOUT_DEFINITIONS } from '../utils/layout';
35
+ import { getValueFromObjectPath, setImmutably } from '../utils/object';
36
+ import { getSetting } from '../settings/get-setting';
37
+ import type {
38
+ BlockStyleVariation,
39
+ BlockType,
40
+ GlobalStylesConfig,
41
+ GlobalStylesSettings,
42
+ GlobalStylesStyles,
43
+ } from '../types';
44
+
45
+ // =============================================================================
46
+ // LOCAL TYPE DEFINITIONS
47
+ // =============================================================================
48
+
49
+ /**
50
+ * Preset metadata for CSS variable generation
51
+ */
52
+ interface PresetMetadata {
53
+ path: string[];
54
+ valueKey?: string;
55
+ valueFunc?: ( preset: any, settings: any ) => string | number | null;
56
+ cssVarInfix: string;
57
+ classes?: Array< {
58
+ classSuffix: string;
59
+ propertyName: string;
60
+ } >;
61
+ }
62
+
63
+ /**
64
+ * Preset collection by origin
65
+ */
66
+ interface PresetsByOrigin {
67
+ [ origin: string ]: any[];
68
+ }
69
+
70
+ /**
71
+ * CSS class configuration
72
+ */
73
+ interface CSSClassConfig {
74
+ classSuffix: string;
75
+ propertyName: string;
76
+ }
77
+
78
+ /**
79
+ * Style property configuration from WordPress
80
+ */
81
+ interface StylePropertyConfig {
82
+ value: string[];
83
+ properties?: Record< string, string >;
84
+ useEngine?: boolean;
85
+ rootOnly?: boolean;
86
+ }
87
+
88
+ /**
89
+ * Layout definition structure
90
+ */
91
+ interface LayoutDefinition {
92
+ className: string;
93
+ name: string;
94
+ displayMode?: string;
95
+ spacingStyles?: Array< {
96
+ selector?: string;
97
+ rules?: Record< string, any >;
98
+ } >;
99
+ baseStyles?: Array< {
100
+ selector?: string;
101
+ rules?: Record< string, any >;
102
+ } >;
103
+ }
104
+
105
+ /**
106
+ * CSS rule from style engine
107
+ */
108
+ interface CSSRule {
109
+ key: string;
110
+ value: any;
111
+ }
112
+
113
+ /**
114
+ * Block variation in theme.json (different from BlockStyleVariation)
115
+ */
116
+ interface BlockVariation {
117
+ css?: string;
118
+ elements?: Record< string, any >;
119
+ blocks?: Record< string, any >;
120
+ [ key: string ]: any; // For additional style properties
121
+ }
122
+
123
+ /**
124
+ * Block node in theme.json
125
+ */
126
+ interface BlockNode {
127
+ variations?: Record< string, BlockVariation >;
128
+ elements?: Record< string, any >;
129
+ [ key: string ]: any; // For additional style properties
130
+ }
131
+
132
+ export type BlockSelectors = Record<
133
+ string,
134
+ {
135
+ duotoneSelector?: string;
136
+ selector: string;
137
+ fallbackGapValue?: string;
138
+ hasLayoutSupport?: boolean;
139
+ featureSelectors?:
140
+ | string
141
+ | Record< string, string | Record< string, string > >;
142
+ name?: string;
143
+ styleVariationSelectors?: Record< string, string >;
144
+ }
145
+ >;
146
+
147
+ // Elements that rely on class names in their selectors.
148
+ const ELEMENT_CLASS_NAMES = {
149
+ button: 'wp-element-button',
150
+ caption: 'wp-element-caption',
151
+ };
152
+
153
+ // List of block support features that can have their related styles
154
+ // generated under their own feature level selector rather than the block's.
155
+ const BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS = {
156
+ __experimentalBorder: 'border',
157
+ color: 'color',
158
+ spacing: 'spacing',
159
+ typography: 'typography',
160
+ };
161
+
162
+ /**
163
+ * Transform given preset tree into a set of style declarations.
164
+ *
165
+ * @param blockPresets Block presets object
166
+ * @param mergedSettings Merged theme.json settings
167
+ * @return An array of style declarations
168
+ */
169
+ function getPresetsDeclarations(
170
+ blockPresets: Record< string, any > = {},
171
+ mergedSettings: GlobalStylesSettings
172
+ ): string[] {
173
+ return PRESET_METADATA.reduce(
174
+ (
175
+ declarations: string[],
176
+ { path, valueKey, valueFunc, cssVarInfix }: PresetMetadata
177
+ ) => {
178
+ const presetByOrigin = getValueFromObjectPath(
179
+ blockPresets,
180
+ path,
181
+ []
182
+ ) as PresetsByOrigin;
183
+ [ 'default', 'theme', 'custom' ].forEach( ( origin ) => {
184
+ if ( presetByOrigin[ origin ] ) {
185
+ presetByOrigin[ origin ].forEach( ( value: any ) => {
186
+ if ( valueKey && ! valueFunc ) {
187
+ declarations.push(
188
+ `--wp--preset--${ cssVarInfix }--${ kebabCase(
189
+ value.slug
190
+ ) }: ${ value[ valueKey ] }`
191
+ );
192
+ } else if (
193
+ valueFunc &&
194
+ typeof valueFunc === 'function'
195
+ ) {
196
+ declarations.push(
197
+ `--wp--preset--${ cssVarInfix }--${ kebabCase(
198
+ value.slug
199
+ ) }: ${ valueFunc( value, mergedSettings ) }`
200
+ );
201
+ }
202
+ } );
203
+ }
204
+ } );
205
+
206
+ return declarations;
207
+ },
208
+ [] as string[]
209
+ );
210
+ }
211
+
212
+ /**
213
+ * Transform given preset tree into a set of preset class declarations.
214
+ *
215
+ * @param blockSelector Block selector string
216
+ * @param blockPresets Block presets object
217
+ * @return CSS declarations for the preset classes
218
+ */
219
+ function getPresetsClasses(
220
+ blockSelector: string = '*',
221
+ blockPresets: Record< string, any > = {}
222
+ ): string {
223
+ return PRESET_METADATA.reduce(
224
+ (
225
+ declarations: string,
226
+ { path, cssVarInfix, classes }: PresetMetadata
227
+ ) => {
228
+ if ( ! classes ) {
229
+ return declarations;
230
+ }
231
+
232
+ const presetByOrigin = getValueFromObjectPath(
233
+ blockPresets,
234
+ path,
235
+ []
236
+ ) as PresetsByOrigin;
237
+ [ 'default', 'theme', 'custom' ].forEach( ( origin ) => {
238
+ if ( presetByOrigin[ origin ] ) {
239
+ presetByOrigin[ origin ].forEach(
240
+ ( { slug }: { slug: string } ) => {
241
+ classes!.forEach(
242
+ ( {
243
+ classSuffix,
244
+ propertyName,
245
+ }: CSSClassConfig ) => {
246
+ const classSelectorToUse = `.has-${ kebabCase(
247
+ slug
248
+ ) }-${ classSuffix }`;
249
+ const selectorToUse = blockSelector
250
+ .split( ',' ) // Selector can be "h1, h2, h3"
251
+ .map(
252
+ ( selector ) =>
253
+ `${ selector }${ classSelectorToUse }`
254
+ )
255
+ .join( ',' );
256
+ const value = `var(--wp--preset--${ cssVarInfix }--${ kebabCase(
257
+ slug
258
+ ) })`;
259
+ declarations += `${ selectorToUse }{${ propertyName }: ${ value } !important;}`;
260
+ }
261
+ );
262
+ }
263
+ );
264
+ }
265
+ } );
266
+ return declarations;
267
+ },
268
+ ''
269
+ );
270
+ }
271
+
272
+ function getPresetsSvgFilters(
273
+ blockPresets: Record< string, any > = {}
274
+ ): string[] {
275
+ return PRESET_METADATA.filter(
276
+ // Duotone are the only type of filters for now.
277
+ ( metadata: PresetMetadata ) => metadata.path.at( -1 ) === 'duotone'
278
+ ).flatMap( ( metadata: PresetMetadata ) => {
279
+ const presetByOrigin = getValueFromObjectPath(
280
+ blockPresets,
281
+ metadata.path,
282
+ {}
283
+ ) as PresetsByOrigin;
284
+ return [ 'default', 'theme' ]
285
+ .filter( ( origin ) => presetByOrigin[ origin ] )
286
+ .flatMap( ( origin ) =>
287
+ presetByOrigin[ origin ].map( ( preset: any ) =>
288
+ getDuotoneFilter(
289
+ `wp-duotone-${ preset.slug }`,
290
+ preset.colors
291
+ )
292
+ )
293
+ )
294
+ .join( '' );
295
+ } );
296
+ }
297
+
298
+ function flattenTree(
299
+ input: any = {},
300
+ prefix: string,
301
+ token: string
302
+ ): string[] {
303
+ let result: string[] = [];
304
+ Object.keys( input ).forEach( ( key ) => {
305
+ const newKey = prefix + kebabCase( key.replace( '/', '-' ) );
306
+ const newLeaf = input[ key ];
307
+
308
+ if ( newLeaf instanceof Object ) {
309
+ const newPrefix = newKey + token;
310
+ result = [ ...result, ...flattenTree( newLeaf, newPrefix, token ) ];
311
+ } else {
312
+ result.push( `${ newKey }: ${ newLeaf }` );
313
+ }
314
+ } );
315
+ return result;
316
+ }
317
+
318
+ /**
319
+ * Gets variation selector string from feature selector.
320
+ *
321
+ * @param featureSelector The feature selector
322
+ * @param styleVariationSelector The style variation selector
323
+ * @return Combined selector string
324
+ */
325
+ function concatFeatureVariationSelectorString(
326
+ featureSelector: string,
327
+ styleVariationSelector: string
328
+ ): string {
329
+ const featureSelectors = featureSelector.split( ',' );
330
+ const combinedSelectors: string[] = [];
331
+ featureSelectors.forEach( ( selector ) => {
332
+ combinedSelectors.push(
333
+ `${ styleVariationSelector.trim() }${ selector.trim() }`
334
+ );
335
+ } );
336
+ return combinedSelectors.join( ', ' );
337
+ }
338
+
339
+ /**
340
+ * Generate style declarations for a block's custom feature and subfeature
341
+ * selectors.
342
+ *
343
+ * NOTE: The passed `styles` object will be mutated by this function.
344
+ *
345
+ * @param selectors Custom selectors object for a block
346
+ * @param styles A block's styles object
347
+ * @return Style declarations
348
+ */
349
+ const getFeatureDeclarations = (
350
+ selectors: Record< string, any >,
351
+ styles: Record< string, any >
352
+ ): Record< string, string[] > => {
353
+ const declarations: Record< string, string[] > = {};
354
+
355
+ Object.entries( selectors ).forEach( ( [ feature, selector ] ) => {
356
+ // We're only processing features/subfeatures that have styles.
357
+ if ( feature === 'root' || ! styles?.[ feature ] ) {
358
+ return;
359
+ }
360
+
361
+ const isShorthand = typeof selector === 'string';
362
+
363
+ // If we have a selector object instead of shorthand process it.
364
+ if (
365
+ ! isShorthand &&
366
+ typeof selector === 'object' &&
367
+ selector !== null
368
+ ) {
369
+ Object.entries( selector as Record< string, string > ).forEach(
370
+ ( [ subfeature, subfeatureSelector ] ) => {
371
+ // Don't process root feature selector yet or any
372
+ // subfeature that doesn't have a style.
373
+ if (
374
+ subfeature === 'root' ||
375
+ ! styles?.[ feature ][ subfeature ]
376
+ ) {
377
+ return;
378
+ }
379
+
380
+ // Create a temporary styles object and build
381
+ // declarations for subfeature.
382
+ const subfeatureStyles = {
383
+ [ feature ]: {
384
+ [ subfeature ]: styles[ feature ][ subfeature ],
385
+ },
386
+ };
387
+ const newDeclarations =
388
+ getStylesDeclarations( subfeatureStyles );
389
+
390
+ // Merge new declarations in with any others that
391
+ // share the same selector.
392
+ declarations[ subfeatureSelector ] = [
393
+ ...( declarations[ subfeatureSelector ] || [] ),
394
+ ...newDeclarations,
395
+ ];
396
+
397
+ // Remove the subfeature's style now it will be
398
+ // included under its own selector not the block's.
399
+ delete styles[ feature ][ subfeature ];
400
+ }
401
+ );
402
+ }
403
+
404
+ // Now subfeatures have been processed and removed, we can
405
+ // process root, or shorthand, feature selectors.
406
+ if (
407
+ isShorthand ||
408
+ ( typeof selector === 'object' &&
409
+ selector !== null &&
410
+ 'root' in selector )
411
+ ) {
412
+ const featureSelector = isShorthand
413
+ ? ( selector as string )
414
+ : ( selector as any ).root;
415
+
416
+ // Create temporary style object and build declarations for feature.
417
+ const featureStyles = { [ feature ]: styles[ feature ] };
418
+ const newDeclarations = getStylesDeclarations( featureStyles );
419
+
420
+ // Merge new declarations with any others that share the selector.
421
+ declarations[ featureSelector ] = [
422
+ ...( declarations[ featureSelector ] || [] ),
423
+ ...newDeclarations,
424
+ ];
425
+
426
+ // Remove the feature from the block's styles now as it will be
427
+ // included under its own selector not the block's.
428
+ delete styles[ feature ];
429
+ }
430
+ } );
431
+
432
+ return declarations;
433
+ };
434
+
435
+ /**
436
+ * Transform given style tree into a set of style declarations.
437
+ *
438
+ * @param blockStyles Block styles
439
+ * @param selector The selector these declarations should attach to
440
+ * @param useRootPaddingAlign Whether to use CSS custom properties in root selector
441
+ * @param tree A theme.json tree containing layout definitions
442
+ * @param disableRootPadding Whether to force disable the root padding styles
443
+ * @return An array of style declarations
444
+ */
445
+ export function getStylesDeclarations(
446
+ blockStyles: any = {},
447
+ selector: string = '',
448
+ useRootPaddingAlign?: boolean,
449
+ tree: any = {},
450
+ disableRootPadding: boolean = false
451
+ ): string[] {
452
+ const isRoot = ROOT_BLOCK_SELECTOR === selector;
453
+ const output = Object.entries(
454
+ STYLE_PROPERTY as Record< string, StylePropertyConfig >
455
+ ).reduce(
456
+ (
457
+ declarations: string[],
458
+ [ key, { value, properties, useEngine, rootOnly } ]: [
459
+ string,
460
+ StylePropertyConfig,
461
+ ]
462
+ ) => {
463
+ if ( rootOnly && ! isRoot ) {
464
+ return declarations;
465
+ }
466
+ const pathToValue = value;
467
+ if ( pathToValue[ 0 ] === 'elements' || useEngine ) {
468
+ return declarations;
469
+ }
470
+
471
+ const styleValue = getValueFromObjectPath(
472
+ blockStyles,
473
+ pathToValue
474
+ );
475
+
476
+ // Root-level padding styles don't currently support strings with CSS shorthand values.
477
+ // This may change: https://github.com/WordPress/gutenberg/issues/40132.
478
+ if (
479
+ key === '--wp--style--root--padding' &&
480
+ ( typeof styleValue === 'string' || ! useRootPaddingAlign )
481
+ ) {
482
+ return declarations;
483
+ }
484
+
485
+ if ( properties && typeof styleValue !== 'string' ) {
486
+ Object.entries( properties ).forEach( ( entry ) => {
487
+ const [ name, prop ] = entry;
488
+
489
+ if (
490
+ ! getValueFromObjectPath( styleValue, [ prop ], false )
491
+ ) {
492
+ // Do not create a declaration
493
+ // for sub-properties that don't have any value.
494
+ return;
495
+ }
496
+
497
+ const cssProperty = name.startsWith( '--' )
498
+ ? name
499
+ : kebabCase( name );
500
+ declarations.push(
501
+ `${ cssProperty }: ${ getCSSValueFromRawStyle(
502
+ getValueFromObjectPath( styleValue, [ prop ] )
503
+ ) }`
504
+ );
505
+ } );
506
+ } else if (
507
+ getValueFromObjectPath( blockStyles, pathToValue, false )
508
+ ) {
509
+ const cssProperty = key.startsWith( '--' )
510
+ ? key
511
+ : kebabCase( key );
512
+ declarations.push(
513
+ `${ cssProperty }: ${ getCSSValueFromRawStyle(
514
+ getValueFromObjectPath( blockStyles, pathToValue )
515
+ ) }`
516
+ );
517
+ }
518
+
519
+ return declarations;
520
+ },
521
+ [] as string[]
522
+ );
523
+
524
+ /*
525
+ * Preprocess background image values.
526
+ *
527
+ * Note: As we absorb more and more styles into the engine, we could simplify this function.
528
+ * A refactor is for the style engine to handle ref resolution (and possibly defaults)
529
+ * via a public util used internally and externally. Theme.json tree and defaults could be passed
530
+ * as options.
531
+ */
532
+ if ( !! blockStyles.background ) {
533
+ /*
534
+ * Resolve dynamic values before they are compiled by the style engine,
535
+ * which doesn't (yet) resolve dynamic values.
536
+ */
537
+ if ( blockStyles.background?.backgroundImage ) {
538
+ blockStyles.background.backgroundImage = getResolvedValue(
539
+ blockStyles.background.backgroundImage,
540
+ tree
541
+ );
542
+ }
543
+
544
+ /*
545
+ * Set default values for block background styles.
546
+ * Top-level styles are an exception as they are applied to the body.
547
+ */
548
+ if ( ! isRoot && !! blockStyles.background?.backgroundImage?.id ) {
549
+ blockStyles = {
550
+ ...blockStyles,
551
+ background: {
552
+ ...blockStyles.background,
553
+ ...setBackgroundStyleDefaults( blockStyles.background ),
554
+ },
555
+ };
556
+ }
557
+ }
558
+
559
+ const extraRules = getCSSRules( blockStyles );
560
+ extraRules.forEach( ( rule: CSSRule ) => {
561
+ // Don't output padding properties if padding variables are set or if we're not editing a full template.
562
+ if (
563
+ isRoot &&
564
+ ( useRootPaddingAlign || disableRootPadding ) &&
565
+ rule.key.startsWith( 'padding' )
566
+ ) {
567
+ return;
568
+ }
569
+ const cssProperty = rule.key.startsWith( '--' )
570
+ ? rule.key
571
+ : kebabCase( rule.key );
572
+
573
+ let ruleValue = getResolvedValue( rule.value, tree );
574
+
575
+ // Calculate fluid typography rules where available.
576
+ if ( cssProperty === 'font-size' ) {
577
+ /*
578
+ * getTypographyFontSizeValue() will check
579
+ * if fluid typography has been activated and also
580
+ * whether the incoming value can be converted to a fluid value.
581
+ * Values that already have a "clamp()" function will not pass the test,
582
+ * and therefore the original $value will be returned.
583
+ */
584
+ ruleValue = getTypographyFontSizeValue(
585
+ { name: '', slug: '', size: ruleValue as string },
586
+ tree?.settings
587
+ );
588
+ }
589
+
590
+ // For aspect ratio to work, other dimensions rules (and Cover block defaults) must be unset.
591
+ // This ensures that a fixed height does not override the aspect ratio.
592
+ if ( cssProperty === 'aspect-ratio' ) {
593
+ output.push( 'min-height: unset' );
594
+ }
595
+
596
+ output.push( `${ cssProperty }: ${ ruleValue }` );
597
+ } );
598
+
599
+ return output;
600
+ }
601
+
602
+ /**
603
+ * Get generated CSS for layout styles by looking up layout definitions provided
604
+ * in theme.json, and outputting common layout styles, and specific blockGap values.
605
+ *
606
+ * @param props Layout styles configuration
607
+ * @param props.layoutDefinitions Layout definitions from theme.json
608
+ * @param props.style Style object for the block
609
+ * @param props.selector Selector to apply the styles to
610
+ * @param props.hasBlockGapSupport Whether the block supports block gap styles
611
+ * @param props.hasFallbackGapSupport Whether the block supports fallback gap styles
612
+ * @param props.fallbackGapValue Fallback gap value to use if block gap support is
613
+ *
614
+ * @return Generated CSS rules for the layout styles
615
+ */
616
+ export function getLayoutStyles( {
617
+ layoutDefinitions = LAYOUT_DEFINITIONS,
618
+ style,
619
+ selector,
620
+ hasBlockGapSupport,
621
+ hasFallbackGapSupport,
622
+ fallbackGapValue,
623
+ }: {
624
+ layoutDefinitions?: Record< string, LayoutDefinition >;
625
+ style?: GlobalStylesStyles;
626
+ selector?: string;
627
+ hasBlockGapSupport?: boolean;
628
+ hasFallbackGapSupport?: boolean;
629
+ fallbackGapValue?: string;
630
+ } ): string {
631
+ let ruleset = '';
632
+ let gapValue = hasBlockGapSupport
633
+ ? getGapCSSValue( style?.spacing?.blockGap )
634
+ : '';
635
+
636
+ // Ensure a fallback gap value for the root layout definitions,
637
+ // and use a fallback value if one is provided for the current block.
638
+ if ( hasFallbackGapSupport ) {
639
+ if ( selector === ROOT_BLOCK_SELECTOR ) {
640
+ gapValue = ! gapValue ? '0.5em' : gapValue;
641
+ } else if ( ! hasBlockGapSupport && fallbackGapValue ) {
642
+ gapValue = fallbackGapValue;
643
+ }
644
+ }
645
+
646
+ if ( gapValue && layoutDefinitions ) {
647
+ Object.values( layoutDefinitions ).forEach(
648
+ ( { className, name, spacingStyles } ) => {
649
+ // Allow outputting fallback gap styles for flex layout type when block gap support isn't available.
650
+ if (
651
+ ! hasBlockGapSupport &&
652
+ 'flex' !== name &&
653
+ 'grid' !== name
654
+ ) {
655
+ return;
656
+ }
657
+
658
+ if ( spacingStyles?.length ) {
659
+ spacingStyles.forEach( ( spacingStyle: any ) => {
660
+ const declarations: string[] = [];
661
+
662
+ if ( spacingStyle.rules ) {
663
+ Object.entries( spacingStyle.rules ).forEach(
664
+ ( [ cssProperty, cssValue ] ) => {
665
+ declarations.push(
666
+ `${ cssProperty }: ${
667
+ cssValue ? cssValue : gapValue
668
+ }`
669
+ );
670
+ }
671
+ );
672
+ }
673
+
674
+ if ( declarations.length ) {
675
+ let combinedSelector = '';
676
+
677
+ if ( ! hasBlockGapSupport ) {
678
+ // For fallback gap styles, use lower specificity, to ensure styles do not unintentionally override theme styles.
679
+ combinedSelector =
680
+ selector === ROOT_BLOCK_SELECTOR
681
+ ? `:where(.${ className }${
682
+ spacingStyle?.selector || ''
683
+ })`
684
+ : `:where(${ selector }.${ className }${
685
+ spacingStyle?.selector || ''
686
+ })`;
687
+ } else {
688
+ combinedSelector =
689
+ selector === ROOT_BLOCK_SELECTOR
690
+ ? `:root :where(.${ className })${
691
+ spacingStyle?.selector || ''
692
+ }`
693
+ : `:root :where(${ selector }-${ className })${
694
+ spacingStyle?.selector || ''
695
+ }`;
696
+ }
697
+ ruleset += `${ combinedSelector } { ${ declarations.join(
698
+ '; '
699
+ ) }; }`;
700
+ }
701
+ } );
702
+ }
703
+ }
704
+ );
705
+ // For backwards compatibility, ensure the legacy block gap CSS variable is still available.
706
+ if ( selector === ROOT_BLOCK_SELECTOR && hasBlockGapSupport ) {
707
+ ruleset += `${ ROOT_CSS_PROPERTIES_SELECTOR } { --wp--style--block-gap: ${ gapValue }; }`;
708
+ }
709
+ }
710
+
711
+ // Output base styles
712
+ if ( selector === ROOT_BLOCK_SELECTOR && layoutDefinitions ) {
713
+ const validDisplayModes = [ 'block', 'flex', 'grid' ];
714
+ Object.values( layoutDefinitions ).forEach(
715
+ ( { className, displayMode, baseStyles }: LayoutDefinition ) => {
716
+ if (
717
+ displayMode &&
718
+ validDisplayModes.includes( displayMode )
719
+ ) {
720
+ ruleset += `${ selector } .${ className } { display:${ displayMode }; }`;
721
+ }
722
+
723
+ if ( baseStyles?.length ) {
724
+ baseStyles.forEach( ( baseStyle: any ) => {
725
+ const declarations: string[] = [];
726
+
727
+ if ( baseStyle.rules ) {
728
+ Object.entries( baseStyle.rules ).forEach(
729
+ ( [ cssProperty, cssValue ] ) => {
730
+ declarations.push(
731
+ `${ cssProperty }: ${ cssValue }`
732
+ );
733
+ }
734
+ );
735
+ }
736
+
737
+ if ( declarations.length ) {
738
+ const combinedSelector = `.${ className }${
739
+ baseStyle?.selector || ''
740
+ }`;
741
+ ruleset += `${ combinedSelector } { ${ declarations.join(
742
+ '; '
743
+ ) }; }`;
744
+ }
745
+ } );
746
+ }
747
+ }
748
+ );
749
+ }
750
+
751
+ return ruleset;
752
+ }
753
+
754
+ const STYLE_KEYS = [
755
+ 'border',
756
+ 'color',
757
+ 'dimensions',
758
+ 'spacing',
759
+ 'typography',
760
+ 'filter',
761
+ 'outline',
762
+ 'shadow',
763
+ 'background',
764
+ ];
765
+
766
+ function pickStyleKeys( treeToPickFrom: any ): any {
767
+ if ( ! treeToPickFrom ) {
768
+ return {};
769
+ }
770
+ const entries = Object.entries( treeToPickFrom );
771
+ const pickedEntries = entries.filter( ( [ key ] ) =>
772
+ STYLE_KEYS.includes( key )
773
+ );
774
+ // clone the style objects so that `getFeatureDeclarations` can remove consumed keys from it
775
+ const clonedEntries = pickedEntries.map( ( [ key, style ] ) => [
776
+ key,
777
+ JSON.parse( JSON.stringify( style ) ),
778
+ ] );
779
+ return Object.fromEntries( clonedEntries );
780
+ }
781
+
782
+ export const getNodesWithStyles = (
783
+ tree: GlobalStylesConfig,
784
+ blockSelectors: string | BlockSelectors
785
+ ): any[] => {
786
+ const nodes: {
787
+ styles: Partial< Omit< GlobalStylesStyles, 'elements' | 'blocks' > >;
788
+ selector: string;
789
+ skipSelectorWrapper?: boolean;
790
+ duotoneSelector?: string;
791
+ featureSelectors?:
792
+ | string
793
+ | Record< string, string | Record< string, string > >;
794
+ fallbackGapValue?: string;
795
+ hasLayoutSupport?: boolean;
796
+ styleVariationSelectors?: Record< string, string >;
797
+ }[] = [];
798
+
799
+ if ( ! tree?.styles ) {
800
+ return nodes;
801
+ }
802
+
803
+ // Top-level.
804
+ const styles = pickStyleKeys( tree.styles );
805
+ if ( styles ) {
806
+ nodes.push( {
807
+ styles,
808
+ selector: ROOT_BLOCK_SELECTOR,
809
+ // Root selector (body) styles should not be wrapped in `:root where()` to keep
810
+ // specificity at (0,0,1) and maintain backwards compatibility.
811
+ skipSelectorWrapper: true,
812
+ } );
813
+ }
814
+
815
+ Object.entries( ELEMENTS ).forEach( ( [ name, selector ] ) => {
816
+ if ( tree.styles?.elements?.[ name ] ) {
817
+ nodes.push( {
818
+ styles: tree.styles?.elements?.[ name ] ?? {},
819
+ selector: selector as string,
820
+ // Top level elements that don't use a class name should not receive the
821
+ // `:root :where()` wrapper to maintain backwards compatibility.
822
+ skipSelectorWrapper: ! (
823
+ ELEMENT_CLASS_NAMES as Record< string, string >
824
+ )[ name ],
825
+ } );
826
+ }
827
+ } );
828
+
829
+ // Iterate over blocks: they can have styles & elements.
830
+ Object.entries( tree.styles?.blocks ?? {} ).forEach(
831
+ ( [ blockName, node ] ) => {
832
+ const blockStyles = pickStyleKeys( node );
833
+ const typedNode = node as BlockNode;
834
+
835
+ if ( typedNode?.variations ) {
836
+ const variations: Record< string, any > = {};
837
+ Object.entries( typedNode.variations ).forEach(
838
+ ( [ variationName, variation ] ) => {
839
+ const typedVariation = variation as BlockVariation;
840
+ variations[ variationName ] =
841
+ pickStyleKeys( typedVariation );
842
+ if ( typedVariation?.css ) {
843
+ variations[ variationName ].css =
844
+ typedVariation.css;
845
+ }
846
+ const variationSelector =
847
+ typeof blockSelectors !== 'string'
848
+ ? blockSelectors[ blockName ]
849
+ ?.styleVariationSelectors?.[
850
+ variationName
851
+ ]
852
+ : undefined;
853
+
854
+ // Process the variation's inner element styles.
855
+ // This comes before the inner block styles so the
856
+ // element styles within the block type styles take
857
+ // precedence over these.
858
+ Object.entries(
859
+ typedVariation?.elements ?? {}
860
+ ).forEach( ( [ element, elementStyles ] ) => {
861
+ if ( elementStyles && ELEMENTS[ element ] ) {
862
+ nodes.push( {
863
+ styles: elementStyles,
864
+ selector: scopeSelector(
865
+ variationSelector,
866
+ ELEMENTS[ element ]
867
+ ),
868
+ } );
869
+ }
870
+ } );
871
+
872
+ // Process the variations inner block type styles.
873
+ Object.entries( typedVariation?.blocks ?? {} ).forEach(
874
+ ( [
875
+ variationBlockName,
876
+ variationBlockStyles,
877
+ ] ) => {
878
+ const variationBlockSelector =
879
+ typeof blockSelectors !== 'string'
880
+ ? scopeSelector(
881
+ variationSelector,
882
+ blockSelectors[
883
+ variationBlockName
884
+ ]?.selector
885
+ )
886
+ : undefined;
887
+ const variationDuotoneSelector =
888
+ typeof blockSelectors !== 'string'
889
+ ? scopeSelector(
890
+ variationSelector,
891
+ blockSelectors[
892
+ variationBlockName
893
+ ]?.duotoneSelector as string
894
+ )
895
+ : undefined;
896
+ const variationFeatureSelectors =
897
+ typeof blockSelectors !== 'string'
898
+ ? scopeFeatureSelectors(
899
+ variationSelector,
900
+ blockSelectors[
901
+ variationBlockName
902
+ ]?.featureSelectors ?? {}
903
+ )
904
+ : undefined;
905
+
906
+ const variationBlockStyleNodes =
907
+ pickStyleKeys( variationBlockStyles );
908
+
909
+ if ( variationBlockStyles?.css ) {
910
+ variationBlockStyleNodes.css =
911
+ variationBlockStyles.css;
912
+ }
913
+
914
+ if (
915
+ ! variationBlockSelector ||
916
+ typeof blockSelectors === 'string'
917
+ ) {
918
+ return;
919
+ }
920
+
921
+ nodes.push( {
922
+ selector: variationBlockSelector,
923
+ duotoneSelector: variationDuotoneSelector,
924
+ featureSelectors: variationFeatureSelectors,
925
+ fallbackGapValue:
926
+ blockSelectors[ variationBlockName ]
927
+ ?.fallbackGapValue,
928
+ hasLayoutSupport:
929
+ blockSelectors[ variationBlockName ]
930
+ ?.hasLayoutSupport,
931
+ styles: variationBlockStyleNodes,
932
+ } );
933
+
934
+ // Process element styles for the inner blocks
935
+ // of the variation.
936
+ Object.entries(
937
+ variationBlockStyles.elements ?? {}
938
+ ).forEach(
939
+ ( [
940
+ variationBlockElement,
941
+ variationBlockElementStyles,
942
+ ] ) => {
943
+ if (
944
+ variationBlockElementStyles &&
945
+ ELEMENTS[ variationBlockElement ]
946
+ ) {
947
+ nodes.push( {
948
+ styles: variationBlockElementStyles,
949
+ selector: scopeSelector(
950
+ variationBlockSelector,
951
+ ELEMENTS[
952
+ variationBlockElement
953
+ ]
954
+ ),
955
+ } );
956
+ }
957
+ }
958
+ );
959
+ }
960
+ );
961
+ }
962
+ );
963
+ blockStyles.variations = variations;
964
+ }
965
+
966
+ if (
967
+ typeof blockSelectors !== 'string' &&
968
+ blockSelectors?.[ blockName ]?.selector
969
+ ) {
970
+ nodes.push( {
971
+ duotoneSelector:
972
+ blockSelectors[ blockName ].duotoneSelector,
973
+ fallbackGapValue:
974
+ blockSelectors[ blockName ].fallbackGapValue,
975
+ hasLayoutSupport:
976
+ blockSelectors[ blockName ].hasLayoutSupport,
977
+ selector: blockSelectors[ blockName ].selector,
978
+ styles: blockStyles,
979
+ featureSelectors:
980
+ blockSelectors[ blockName ].featureSelectors,
981
+ styleVariationSelectors:
982
+ blockSelectors[ blockName ].styleVariationSelectors,
983
+ } );
984
+ }
985
+
986
+ Object.entries( typedNode?.elements ?? {} ).forEach(
987
+ ( [ elementName, value ] ) => {
988
+ if (
989
+ typeof blockSelectors !== 'string' &&
990
+ value &&
991
+ blockSelectors?.[ blockName ] &&
992
+ ELEMENTS[ elementName ]
993
+ ) {
994
+ nodes.push( {
995
+ styles: value,
996
+ selector: blockSelectors[ blockName ]?.selector
997
+ .split( ',' )
998
+ .map( ( sel: string ) => {
999
+ const elementSelectors =
1000
+ ELEMENTS[ elementName ].split( ',' );
1001
+ return elementSelectors.map(
1002
+ ( elementSelector: string ) =>
1003
+ sel + ' ' + elementSelector
1004
+ );
1005
+ } )
1006
+ .join( ',' ),
1007
+ } );
1008
+ }
1009
+ }
1010
+ );
1011
+ }
1012
+ );
1013
+
1014
+ return nodes;
1015
+ };
1016
+
1017
+ export const getNodesWithSettings = (
1018
+ tree: GlobalStylesConfig,
1019
+ blockSelectors: string | BlockSelectors
1020
+ ): any[] => {
1021
+ const nodes: {
1022
+ presets: Record< string, any >;
1023
+ custom?: Record< string, any >;
1024
+ selector?: string;
1025
+ duotoneSelector?: string;
1026
+ fallbackGapValue?: string;
1027
+ hasLayoutSupport?: boolean;
1028
+ featureSelectors?: Record< string, string >;
1029
+ styleVariationSelectors?: Record< string, string >;
1030
+ }[] = [];
1031
+
1032
+ if ( ! tree?.settings ) {
1033
+ return nodes;
1034
+ }
1035
+
1036
+ const pickPresets = ( treeToPickFrom: any ): any => {
1037
+ let presets = {};
1038
+ PRESET_METADATA.forEach( ( { path } ) => {
1039
+ const value = getValueFromObjectPath( treeToPickFrom, path, false );
1040
+ if ( value !== false ) {
1041
+ presets = setImmutably( presets, path, value );
1042
+ }
1043
+ } );
1044
+ return presets;
1045
+ };
1046
+
1047
+ // Top-level.
1048
+ const presets = pickPresets( tree.settings );
1049
+ const custom = tree.settings?.custom;
1050
+ if ( Object.keys( presets ).length > 0 || custom ) {
1051
+ nodes.push( {
1052
+ presets,
1053
+ custom,
1054
+ selector: ROOT_CSS_PROPERTIES_SELECTOR,
1055
+ } );
1056
+ }
1057
+
1058
+ // Blocks.
1059
+ Object.entries( tree.settings?.blocks ?? {} ).forEach(
1060
+ ( [ blockName, node ] ) => {
1061
+ const blockCustom = node.custom;
1062
+ if (
1063
+ typeof blockSelectors === 'string' ||
1064
+ ! blockSelectors[ blockName ]
1065
+ ) {
1066
+ return;
1067
+ }
1068
+ const blockPresets = pickPresets( node );
1069
+ if ( Object.keys( blockPresets ).length > 0 || blockCustom ) {
1070
+ nodes.push( {
1071
+ presets: blockPresets,
1072
+ custom: blockCustom,
1073
+ selector: blockSelectors[ blockName ]?.selector,
1074
+ } );
1075
+ }
1076
+ }
1077
+ );
1078
+
1079
+ return nodes;
1080
+ };
1081
+
1082
+ export const generateCustomProperties = (
1083
+ tree: GlobalStylesConfig,
1084
+ blockSelectors: BlockSelectors
1085
+ ): string => {
1086
+ const settings = getNodesWithSettings( tree, blockSelectors );
1087
+ let ruleset = '';
1088
+ settings.forEach( ( { presets, custom, selector } ) => {
1089
+ const declarations = tree?.settings
1090
+ ? getPresetsDeclarations( presets, tree?.settings )
1091
+ : [];
1092
+ const customProps = flattenTree( custom, '--wp--custom--', '--' );
1093
+ if ( customProps.length > 0 ) {
1094
+ declarations.push( ...customProps );
1095
+ }
1096
+
1097
+ if ( declarations.length > 0 ) {
1098
+ ruleset += `${ selector }{${ declarations.join( ';' ) };}`;
1099
+ }
1100
+ } );
1101
+
1102
+ return ruleset;
1103
+ };
1104
+
1105
+ export const transformToStyles = (
1106
+ tree: GlobalStylesConfig,
1107
+ blockSelectors: string | BlockSelectors,
1108
+ hasBlockGapSupport?: boolean,
1109
+ hasFallbackGapSupport?: boolean,
1110
+ disableLayoutStyles: boolean = false,
1111
+ disableRootPadding: boolean = false,
1112
+ styleOptions: Record< string, boolean > = {}
1113
+ ): string => {
1114
+ // These allow opting out of certain sets of styles.
1115
+ const options = {
1116
+ blockGap: true,
1117
+ blockStyles: true,
1118
+ layoutStyles: true,
1119
+ marginReset: true,
1120
+ presets: true,
1121
+ rootPadding: true,
1122
+ variationStyles: false,
1123
+ ...styleOptions,
1124
+ };
1125
+ const nodesWithStyles = getNodesWithStyles( tree, blockSelectors );
1126
+ const nodesWithSettings = getNodesWithSettings( tree, blockSelectors );
1127
+ const useRootPaddingAlign = tree?.settings?.useRootPaddingAwareAlignments;
1128
+ const { contentSize, wideSize } = tree?.settings?.layout || {};
1129
+ const hasBodyStyles =
1130
+ options.marginReset || options.rootPadding || options.layoutStyles;
1131
+
1132
+ let ruleset = '';
1133
+
1134
+ if ( options.presets && ( contentSize || wideSize ) ) {
1135
+ ruleset += `${ ROOT_CSS_PROPERTIES_SELECTOR } {`;
1136
+ ruleset = contentSize
1137
+ ? ruleset + ` --wp--style--global--content-size: ${ contentSize };`
1138
+ : ruleset;
1139
+ ruleset = wideSize
1140
+ ? ruleset + ` --wp--style--global--wide-size: ${ wideSize };`
1141
+ : ruleset;
1142
+ ruleset += '}';
1143
+ }
1144
+
1145
+ if ( hasBodyStyles ) {
1146
+ /*
1147
+ * Reset default browser margin on the body element.
1148
+ * This is set on the body selector **before** generating the ruleset
1149
+ * from the `theme.json`. This is to ensure that if the `theme.json` declares
1150
+ * `margin` in its `spacing` declaration for the `body` element then these
1151
+ * user-generated values take precedence in the CSS cascade.
1152
+ * @link https://github.com/WordPress/gutenberg/issues/36147.
1153
+ */
1154
+ ruleset += ':where(body) {margin: 0;';
1155
+
1156
+ // Root padding styles should be output for full templates, patterns and template parts.
1157
+ if ( options.rootPadding && useRootPaddingAlign ) {
1158
+ /*
1159
+ * These rules reproduce the ones from https://github.com/WordPress/gutenberg/blob/79103f124925d1f457f627e154f52a56228ed5ad/lib/class-wp-theme-json-gutenberg.php#L2508
1160
+ * almost exactly, but for the selectors that target block wrappers in the front end. This code only runs in the editor, so it doesn't need those selectors.
1161
+ */
1162
+ ruleset += `padding-right: 0; padding-left: 0; padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom) }
1163
+ .has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }
1164
+ .has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }
1165
+ .has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) { padding-right: 0; padding-left: 0; }
1166
+ .has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) > .alignfull { margin-left: 0; margin-right: 0;
1167
+ `;
1168
+ }
1169
+
1170
+ ruleset += '}';
1171
+ }
1172
+
1173
+ if ( options.blockStyles ) {
1174
+ nodesWithStyles.forEach(
1175
+ ( {
1176
+ selector,
1177
+ duotoneSelector,
1178
+ styles,
1179
+ fallbackGapValue,
1180
+ hasLayoutSupport,
1181
+ featureSelectors,
1182
+ styleVariationSelectors,
1183
+ skipSelectorWrapper,
1184
+ } ) => {
1185
+ // Process styles for block support features with custom feature level
1186
+ // CSS selectors set.
1187
+ if ( featureSelectors ) {
1188
+ const featureDeclarations = getFeatureDeclarations(
1189
+ featureSelectors,
1190
+ styles
1191
+ );
1192
+
1193
+ Object.entries( featureDeclarations ).forEach(
1194
+ ( [ cssSelector, declarations ] ) => {
1195
+ if ( declarations.length ) {
1196
+ const rules = declarations.join( ';' );
1197
+ ruleset += `:root :where(${ cssSelector }){${ rules };}`;
1198
+ }
1199
+ }
1200
+ );
1201
+ }
1202
+
1203
+ // Process duotone styles.
1204
+ if ( duotoneSelector ) {
1205
+ const duotoneStyles: any = {};
1206
+ if ( styles?.filter ) {
1207
+ duotoneStyles.filter = styles.filter;
1208
+ delete styles.filter;
1209
+ }
1210
+ const duotoneDeclarations =
1211
+ getStylesDeclarations( duotoneStyles );
1212
+ if ( duotoneDeclarations.length ) {
1213
+ ruleset += `${ duotoneSelector }{${ duotoneDeclarations.join(
1214
+ ';'
1215
+ ) };}`;
1216
+ }
1217
+ }
1218
+
1219
+ // Process blockGap and layout styles.
1220
+ if (
1221
+ ! disableLayoutStyles &&
1222
+ ( ROOT_BLOCK_SELECTOR === selector || hasLayoutSupport )
1223
+ ) {
1224
+ ruleset += getLayoutStyles( {
1225
+ style: styles,
1226
+ selector,
1227
+ hasBlockGapSupport,
1228
+ hasFallbackGapSupport,
1229
+ fallbackGapValue,
1230
+ } );
1231
+ }
1232
+
1233
+ // Process the remaining block styles (they use either normal block class or __experimentalSelector).
1234
+ const styleDeclarations = getStylesDeclarations(
1235
+ styles,
1236
+ selector,
1237
+ useRootPaddingAlign,
1238
+ tree,
1239
+ disableRootPadding
1240
+ );
1241
+ if ( styleDeclarations?.length ) {
1242
+ const generalSelector = skipSelectorWrapper
1243
+ ? selector
1244
+ : `:root :where(${ selector })`;
1245
+ ruleset += `${ generalSelector }{${ styleDeclarations.join(
1246
+ ';'
1247
+ ) };}`;
1248
+ }
1249
+ if ( styles?.css ) {
1250
+ ruleset += processCSSNesting(
1251
+ styles.css,
1252
+ `:root :where(${ selector })`
1253
+ );
1254
+ }
1255
+
1256
+ if ( options.variationStyles && styleVariationSelectors ) {
1257
+ Object.entries( styleVariationSelectors ).forEach(
1258
+ ( [ styleVariationName, styleVariationSelector ] ) => {
1259
+ const styleVariations =
1260
+ styles?.variations?.[ styleVariationName ];
1261
+ if ( styleVariations ) {
1262
+ // If the block uses any custom selectors for block support, add those first.
1263
+ if ( featureSelectors ) {
1264
+ const featureDeclarations =
1265
+ getFeatureDeclarations(
1266
+ featureSelectors,
1267
+ styleVariations
1268
+ );
1269
+
1270
+ Object.entries(
1271
+ featureDeclarations
1272
+ ).forEach(
1273
+ ( [ baseSelector, declarations ]: [
1274
+ string,
1275
+ string[],
1276
+ ] ) => {
1277
+ if ( declarations.length ) {
1278
+ const cssSelector =
1279
+ concatFeatureVariationSelectorString(
1280
+ baseSelector,
1281
+ styleVariationSelector as string
1282
+ );
1283
+ const rules =
1284
+ declarations.join( ';' );
1285
+ ruleset += `:root :where(${ cssSelector }){${ rules };}`;
1286
+ }
1287
+ }
1288
+ );
1289
+ }
1290
+
1291
+ // Otherwise add regular selectors.
1292
+ const styleVariationDeclarations =
1293
+ getStylesDeclarations(
1294
+ styleVariations,
1295
+ styleVariationSelector as string,
1296
+ useRootPaddingAlign,
1297
+ tree
1298
+ );
1299
+ if ( styleVariationDeclarations.length ) {
1300
+ ruleset += `:root :where(${ styleVariationSelector }){${ styleVariationDeclarations.join(
1301
+ ';'
1302
+ ) };}`;
1303
+ }
1304
+ if ( styleVariations?.css ) {
1305
+ ruleset += processCSSNesting(
1306
+ styleVariations.css,
1307
+ `:root :where(${ styleVariationSelector })`
1308
+ );
1309
+ }
1310
+ }
1311
+ }
1312
+ );
1313
+ }
1314
+
1315
+ // Check for pseudo selector in `styles` and handle separately.
1316
+ const pseudoSelectorStyles = Object.entries( styles ).filter(
1317
+ ( [ key ] ) => key.startsWith( ':' )
1318
+ );
1319
+
1320
+ if ( pseudoSelectorStyles?.length ) {
1321
+ pseudoSelectorStyles.forEach(
1322
+ ( [ pseudoKey, pseudoStyle ] ) => {
1323
+ const pseudoDeclarations =
1324
+ getStylesDeclarations( pseudoStyle );
1325
+
1326
+ if ( ! pseudoDeclarations?.length ) {
1327
+ return;
1328
+ }
1329
+
1330
+ // `selector` may be provided in a form
1331
+ // where block level selectors have sub element
1332
+ // selectors appended to them as a comma separated
1333
+ // string.
1334
+ // e.g. `h1 a,h2 a,h3 a,h4 a,h5 a,h6 a`;
1335
+ // Split and append pseudo selector to create
1336
+ // the proper rules to target the elements.
1337
+ const _selector = selector
1338
+ .split( ',' )
1339
+ .map( ( sel: string ) => sel + pseudoKey )
1340
+ .join( ',' );
1341
+
1342
+ // As pseudo classes such as :hover, :focus etc. have class-level
1343
+ // specificity, they must use the `:root :where()` wrapper. This.
1344
+ // caps the specificity at `0-1-0` to allow proper nesting of variations
1345
+ // and block type element styles.
1346
+ const pseudoRule = `:root :where(${ _selector }){${ pseudoDeclarations.join(
1347
+ ';'
1348
+ ) };}`;
1349
+
1350
+ ruleset += pseudoRule;
1351
+ }
1352
+ );
1353
+ }
1354
+ }
1355
+ );
1356
+ }
1357
+
1358
+ if ( options.layoutStyles ) {
1359
+ /* Add alignment / layout styles */
1360
+ ruleset =
1361
+ ruleset +
1362
+ '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
1363
+ ruleset =
1364
+ ruleset +
1365
+ '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
1366
+ ruleset =
1367
+ ruleset +
1368
+ '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
1369
+ }
1370
+
1371
+ if ( options.blockGap && hasBlockGapSupport ) {
1372
+ // Use fallback of `0.5em` just in case, however if there is blockGap support, there should nearly always be a real value.
1373
+ const gapValue =
1374
+ getGapCSSValue( tree?.styles?.spacing?.blockGap ) || '0.5em';
1375
+ ruleset =
1376
+ ruleset +
1377
+ `:root :where(.wp-site-blocks) > * { margin-block-start: ${ gapValue }; margin-block-end: 0; }`;
1378
+ ruleset =
1379
+ ruleset +
1380
+ ':root :where(.wp-site-blocks) > :first-child { margin-block-start: 0; }';
1381
+ ruleset =
1382
+ ruleset +
1383
+ ':root :where(.wp-site-blocks) > :last-child { margin-block-end: 0; }';
1384
+ }
1385
+
1386
+ if ( options.presets ) {
1387
+ nodesWithSettings.forEach( ( { selector, presets } ) => {
1388
+ if (
1389
+ ROOT_BLOCK_SELECTOR === selector ||
1390
+ ROOT_CSS_PROPERTIES_SELECTOR === selector
1391
+ ) {
1392
+ // Do not add extra specificity for top-level classes.
1393
+ selector = '';
1394
+ }
1395
+
1396
+ const classes = getPresetsClasses( selector, presets );
1397
+ if ( classes.length > 0 ) {
1398
+ ruleset += classes;
1399
+ }
1400
+ } );
1401
+ }
1402
+
1403
+ return ruleset;
1404
+ };
1405
+
1406
+ export function generateSvgFilters(
1407
+ tree: GlobalStylesConfig,
1408
+ blockSelectors: BlockSelectors
1409
+ ): string[] {
1410
+ const nodesWithSettings = getNodesWithSettings( tree, blockSelectors );
1411
+ return nodesWithSettings.flatMap( ( { presets } ) => {
1412
+ return getPresetsSvgFilters( presets );
1413
+ } );
1414
+ }
1415
+
1416
+ const getSelectorsConfig = ( blockType: BlockType, rootSelector: string ) => {
1417
+ if (
1418
+ blockType?.selectors &&
1419
+ Object.keys( blockType.selectors ).length > 0
1420
+ ) {
1421
+ return blockType.selectors;
1422
+ }
1423
+
1424
+ const config: Record< string, string > = {
1425
+ root: rootSelector,
1426
+ };
1427
+ Object.entries( BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS ).forEach(
1428
+ ( [ featureKey, featureName ] ) => {
1429
+ const featureSelector = getBlockSelector( blockType, featureKey );
1430
+
1431
+ if ( featureSelector ) {
1432
+ config[ featureName ] = featureSelector;
1433
+ }
1434
+ }
1435
+ );
1436
+
1437
+ return config;
1438
+ };
1439
+
1440
+ export const getBlockSelectors = (
1441
+ blockTypes: BlockType[],
1442
+ variationInstanceId?: string
1443
+ ) => {
1444
+ const { getBlockStyles } = select( blocksStore );
1445
+ const result: BlockSelectors = {};
1446
+ blockTypes.forEach( ( blockType ) => {
1447
+ const name = blockType.name;
1448
+ const selector = getBlockSelector( blockType );
1449
+
1450
+ if ( ! selector ) {
1451
+ return; // Skip blocks without valid selectors
1452
+ }
1453
+ let duotoneSelector = getBlockSelector( blockType, 'filter.duotone' );
1454
+ // Keep backwards compatibility for support.color.__experimentalDuotone.
1455
+ if ( ! duotoneSelector ) {
1456
+ const rootSelector = getBlockSelector( blockType );
1457
+ const duotoneSupport = getBlockSupport(
1458
+ blockType,
1459
+ 'color.__experimentalDuotone',
1460
+ false
1461
+ );
1462
+ duotoneSelector =
1463
+ duotoneSupport &&
1464
+ rootSelector &&
1465
+ scopeSelector( rootSelector, duotoneSupport );
1466
+ }
1467
+
1468
+ const hasLayoutSupport =
1469
+ !! blockType?.supports?.layout ||
1470
+ !! blockType?.supports?.__experimentalLayout;
1471
+ const fallbackGapValue =
1472
+ // @ts-expect-error
1473
+ blockType?.supports?.spacing?.blockGap?.__experimentalDefault;
1474
+
1475
+ const blockStyleVariations = getBlockStyles( name );
1476
+ const styleVariationSelectors: Record< string, string > = {};
1477
+ blockStyleVariations?.forEach( ( variation: BlockStyleVariation ) => {
1478
+ const variationSuffix = variationInstanceId
1479
+ ? `-${ variationInstanceId }`
1480
+ : '';
1481
+ const variationName = `${ variation.name }${ variationSuffix }`;
1482
+ const styleVariationSelector = getBlockStyleVariationSelector(
1483
+ variationName,
1484
+ selector
1485
+ );
1486
+
1487
+ styleVariationSelectors[ variationName ] = styleVariationSelector;
1488
+ } );
1489
+
1490
+ // For each block support feature add any custom selectors.
1491
+ const featureSelectors = getSelectorsConfig( blockType, selector );
1492
+
1493
+ result[ name ] = {
1494
+ duotoneSelector: duotoneSelector ?? undefined,
1495
+ fallbackGapValue,
1496
+ featureSelectors: Object.keys( featureSelectors ).length
1497
+ ? featureSelectors
1498
+ : undefined,
1499
+ hasLayoutSupport,
1500
+ name,
1501
+ selector,
1502
+ styleVariationSelectors: blockStyleVariations?.length
1503
+ ? styleVariationSelectors
1504
+ : undefined,
1505
+ };
1506
+ } );
1507
+
1508
+ return result;
1509
+ };
1510
+
1511
+ /**
1512
+ * If there is a separator block whose color is defined in theme.json via background,
1513
+ * update the separator color to the same value by using border color.
1514
+ *
1515
+ * @param config Theme.json configuration file object
1516
+ * @return Theme.json configuration file object updated
1517
+ */
1518
+ function updateConfigWithSeparator(
1519
+ config: GlobalStylesConfig
1520
+ ): GlobalStylesConfig {
1521
+ const blocks = config.styles?.blocks;
1522
+ const separatorBlock = blocks?.[ 'core/separator' ];
1523
+ const needsSeparatorStyleUpdate =
1524
+ separatorBlock &&
1525
+ separatorBlock.color?.background &&
1526
+ ! separatorBlock.color?.text &&
1527
+ ! separatorBlock.border?.color;
1528
+ if ( needsSeparatorStyleUpdate ) {
1529
+ return {
1530
+ ...config,
1531
+ styles: {
1532
+ ...config.styles,
1533
+ blocks: {
1534
+ ...blocks,
1535
+ 'core/separator': {
1536
+ ...separatorBlock,
1537
+ color: {
1538
+ ...separatorBlock.color,
1539
+ text: separatorBlock.color?.background,
1540
+ },
1541
+ },
1542
+ },
1543
+ },
1544
+ };
1545
+ }
1546
+ return config;
1547
+ }
1548
+
1549
+ export function processCSSNesting( css: string, blockSelector: string ) {
1550
+ let processedCSS = '';
1551
+
1552
+ if ( ! css || css.trim() === '' ) {
1553
+ return processedCSS;
1554
+ }
1555
+
1556
+ // Split CSS nested rules.
1557
+ const parts = css.split( '&' );
1558
+ parts.forEach( ( part: string ) => {
1559
+ if ( ! part || part.trim() === '' ) {
1560
+ return;
1561
+ }
1562
+
1563
+ const isRootCss = ! part.includes( '{' );
1564
+ if ( isRootCss ) {
1565
+ // If the part doesn't contain braces, it applies to the root level.
1566
+ processedCSS += `:root :where(${ blockSelector }){${ part.trim() }}`;
1567
+ } else {
1568
+ // If the part contains braces, it's a nested CSS rule.
1569
+ const splitPart = part.replace( '}', '' ).split( '{' );
1570
+ if ( splitPart.length !== 2 ) {
1571
+ return;
1572
+ }
1573
+
1574
+ const [ nestedSelector, cssValue ] = splitPart;
1575
+
1576
+ // Handle pseudo elements such as ::before, ::after, etc. Regex will also
1577
+ // capture any leading combinator such as >, +, or ~, as well as spaces.
1578
+ // This allows pseudo elements as descendants e.g. `.parent ::before`.
1579
+ const matches = nestedSelector.match( /([>+~\s]*::[a-zA-Z-]+)/ );
1580
+ const pseudoPart = matches ? matches[ 1 ] : '';
1581
+ const withoutPseudoElement = matches
1582
+ ? nestedSelector.replace( pseudoPart, '' ).trim()
1583
+ : nestedSelector.trim();
1584
+
1585
+ let combinedSelector;
1586
+ if ( withoutPseudoElement === '' ) {
1587
+ // Only contained a pseudo element to use the block selector to form
1588
+ // the final `:root :where()` selector.
1589
+ combinedSelector = blockSelector;
1590
+ } else {
1591
+ // If the nested selector is a descendant of the block scope it with the
1592
+ // block selector. Otherwise append it to the block selector.
1593
+ combinedSelector = nestedSelector.startsWith( ' ' )
1594
+ ? scopeSelector( blockSelector, withoutPseudoElement )
1595
+ : appendToSelector( blockSelector, withoutPseudoElement );
1596
+ }
1597
+
1598
+ // Build final rule, re-adding any pseudo element outside the `:where()`
1599
+ // to maintain valid CSS selector.
1600
+ processedCSS += `:root :where(${ combinedSelector })${ pseudoPart }{${ cssValue.trim() }}`;
1601
+ }
1602
+ } );
1603
+ return processedCSS;
1604
+ }
1605
+
1606
+ export interface GlobalStylesRenderOptions {
1607
+ hasBlockGapSupport?: boolean;
1608
+ hasFallbackGapSupport?: boolean;
1609
+ disableLayoutStyles?: boolean;
1610
+ disableRootPadding?: boolean;
1611
+ getBlockStyles?: ( blockName: string ) => any[];
1612
+ }
1613
+
1614
+ /**
1615
+ * Returns the global styles output based on the current state of global styles config loaded in the editor context.
1616
+ *
1617
+ * @param config Global styles configuration
1618
+ * @param blockTypes Array of block types from WordPress blocks store
1619
+ * @param options Options for rendering global styles
1620
+ * @return Array of stylesheets and settings
1621
+ */
1622
+ export function generateGlobalStyles(
1623
+ config: GlobalStylesConfig | undefined = {},
1624
+ blockTypes: any[] = [],
1625
+ options: GlobalStylesRenderOptions = {}
1626
+ ): [ any[], any ] {
1627
+ const {
1628
+ hasBlockGapSupport: hasBlockGapSupportOption,
1629
+ hasFallbackGapSupport: hasFallbackGapSupportOption,
1630
+ disableLayoutStyles = false,
1631
+ disableRootPadding = false,
1632
+ } = options;
1633
+
1634
+ // Use provided block types or fall back to getBlockTypes()
1635
+ const blocks = blockTypes.length > 0 ? blockTypes : getBlockTypes();
1636
+
1637
+ const blockGap = getSetting( config, 'spacing.blockGap' );
1638
+ const hasBlockGapSupport = hasBlockGapSupportOption ?? blockGap !== null;
1639
+ const hasFallbackGapSupport =
1640
+ hasFallbackGapSupportOption ?? ! hasBlockGapSupport;
1641
+
1642
+ if ( ! config?.styles || ! config?.settings ) {
1643
+ return [ [], {} ];
1644
+ }
1645
+ const updatedConfig = updateConfigWithSeparator( config );
1646
+ const blockSelectors = getBlockSelectors( blocks );
1647
+ const customProperties = generateCustomProperties(
1648
+ updatedConfig,
1649
+ blockSelectors
1650
+ );
1651
+ const globalStyles = transformToStyles(
1652
+ updatedConfig,
1653
+ blockSelectors,
1654
+ hasBlockGapSupport,
1655
+ hasFallbackGapSupport,
1656
+ disableLayoutStyles,
1657
+ disableRootPadding
1658
+ );
1659
+ const svgs = generateSvgFilters( updatedConfig, blockSelectors );
1660
+ const styles = [
1661
+ {
1662
+ css: customProperties,
1663
+ isGlobalStyles: true,
1664
+ },
1665
+ {
1666
+ css: globalStyles,
1667
+ isGlobalStyles: true,
1668
+ },
1669
+ // Load custom CSS in own stylesheet so that any invalid CSS entered in the input won't break all the global styles in the editor.
1670
+ {
1671
+ css: updatedConfig?.styles?.css ?? '',
1672
+ isGlobalStyles: true,
1673
+ },
1674
+ {
1675
+ assets: svgs,
1676
+ __unstableType: 'svg',
1677
+ isGlobalStyles: true,
1678
+ },
1679
+ ];
1680
+
1681
+ // Loop through the blocks to check if there are custom CSS values.
1682
+ // If there are, get the block selector and push the selector together with
1683
+ // the CSS value to the 'stylesheets' array.
1684
+ blocks.forEach( ( blockType: BlockType ) => {
1685
+ const blockStyles = updatedConfig?.styles?.blocks?.[ blockType.name ];
1686
+ if ( blockStyles?.css ) {
1687
+ const selector = blockSelectors[ blockType.name ].selector;
1688
+ styles.push( {
1689
+ css: processCSSNesting( blockStyles.css, selector ),
1690
+ isGlobalStyles: true,
1691
+ } );
1692
+ }
1693
+ } );
1694
+
1695
+ return [ styles, updatedConfig.settings ];
1696
+ }