1<?php
2/**
3 * Style Engine: WP_Style_Engine_Processor class
4 *
5 * @package WordPress
6 * @subpackage StyleEngine
7 * @since 6.1.0
8 */
9
10/**
11 * Core class used to compile styles from stores or collection of CSS rules.
12 *
13 * @since 6.1.0
14 */
15#[AllowDynamicProperties]
16class WP_Style_Engine_Processor {
17
18 /**
19 * A collection of Style Engine Store objects.
20 *
21 * @since 6.1.0
22 * @var WP_Style_Engine_CSS_Rules_Store[]
23 */
24 protected $stores = array();
25
26 /**
27 * The set of CSS rules that this processor will work on.
28 *
29 * @since 6.1.0
30 * @var WP_Style_Engine_CSS_Rule[]
31 */
32 protected $css_rules = array();
33
34 /**
35 * Adds a store to the processor.
36 *
37 * @since 6.1.0
38 *
39 * @param WP_Style_Engine_CSS_Rules_Store $store The store to add.
40 * @return WP_Style_Engine_Processor Returns the object to allow chaining methods.
41 */
42 public function add_store( $store ) {
43 if ( ! $store instanceof WP_Style_Engine_CSS_Rules_Store ) {
44 _doing_it_wrong(
45 __METHOD__,
46 __( '$store must be an instance of WP_Style_Engine_CSS_Rules_Store' ),
47 '6.1.0'
48 );
49 return $this;
50 }
51
52 $this->stores[ $store->get_name() ] = $store;
53
54 return $this;
55 }
56
57 /**
58 * Adds rules to be processed.
59 *
60 * @since 6.1.0
61 * @since 6.6.0 Added support for rules_group.
62 *
63 * @param WP_Style_Engine_CSS_Rule|WP_Style_Engine_CSS_Rule[] $css_rules A single, or an array of,
64 * WP_Style_Engine_CSS_Rule objects
65 * from a store or otherwise.
66 * @return WP_Style_Engine_Processor Returns the object to allow chaining methods.
67 */
68 public function add_rules( $css_rules ) {
69 if ( ! is_array( $css_rules ) ) {
70 $css_rules = array( $css_rules );
71 }
72
73 foreach ( $css_rules as $rule ) {
74 $selector = $rule->get_selector();
75 $rules_group = $rule->get_rules_group();
76
77 /**
78 * If there is a rules_group and it already exists in the css_rules array,
79 * add the rule to it.
80 * Otherwise, create a new entry for the rules_group.
81 */
82 if ( ! empty( $rules_group ) ) {
83 if ( isset( $this->css_rules[ "$rules_group $selector" ] ) ) {
84 $this->css_rules[ "$rules_group $selector" ]->add_declarations( $rule->get_declarations() );
85 continue;
86 }
87 $this->css_rules[ "$rules_group $selector" ] = $rule;
88 continue;
89 }
90
91 // If the selector already exists, add the declarations to it.
92 if ( isset( $this->css_rules[ $selector ] ) ) {
93 $this->css_rules[ $selector ]->add_declarations( $rule->get_declarations() );
94 continue;
95 }
96 $this->css_rules[ $rule->get_selector() ] = $rule;
97 }
98
99 return $this;
100 }
101
102 /**
103 * Gets the CSS rules as a string.
104 *
105 * @since 6.1.0
106 * @since 6.4.0 The Optimization is no longer the default.
107 *
108 * @param array $options {
109 * Optional. An array of options. Default empty array.
110 *
111 * @type bool $optimize Whether to optimize the CSS output, e.g. combine rules.
112 * Default false.
113 * @type bool $prettify Whether to add new lines and indents to output.
114 * Defaults to whether the `SCRIPT_DEBUG` constant is defined.
115 * }
116 * @return string The computed CSS.
117 */
118 public function get_css( $options = array() ) {
119 $defaults = array(
120 'optimize' => false,
121 'prettify' => defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG,
122 );
123 $options = wp_parse_args( $options, $defaults );
124
125 // If we have stores, get the rules from them.
126 foreach ( $this->stores as $store ) {
127 $this->add_rules( $store->get_all_rules() );
128 }
129
130 // Combine CSS selectors that have identical declarations.
131 if ( true === $options['optimize'] ) {
132 $this->combine_rules_selectors();
133 }
134
135 // Build the CSS.
136 $css = '';
137 foreach ( $this->css_rules as $rule ) {
138 // See class WP_Style_Engine_CSS_Rule for the get_css method.
139 $css .= $rule->get_css( $options['prettify'] );
140 $css .= $options['prettify'] ? "\n" : '';
141 }
142 return $css;
143 }
144
145 /**
146 * Combines selectors from the rules store when they have the same styles.
147 *
148 * @since 6.1.0
149 */
150 private function combine_rules_selectors() {
151 // Build an array of selectors along with the JSON-ified styles to make comparisons easier.
152 $selectors_json = array();
153 foreach ( $this->css_rules as $rule ) {
154 $declarations = $rule->get_declarations()->get_declarations();
155 ksort( $declarations );
156 $selectors_json[ $rule->get_selector() ] = wp_json_encode( $declarations );
157 }
158
159 // Combine selectors that have the same styles.
160 foreach ( $selectors_json as $selector => $json ) {
161 // Get selectors that use the same styles.
162 $duplicates = array_keys( $selectors_json, $json, true );
163 // Skip if there are no duplicates.
164 if ( 1 >= count( $duplicates ) ) {
165 continue;
166 }
167
168 $declarations = $this->css_rules[ $selector ]->get_declarations();
169
170 foreach ( $duplicates as $key ) {
171 // Unset the duplicates from the $selectors_json array to avoid looping through them as well.
172 unset( $selectors_json[ $key ] );
173 // Remove the rules from the rules collection.
174 unset( $this->css_rules[ $key ] );
175 }
176 // Create a new rule with the combined selectors.
177 $duplicate_selectors = implode( ',', $duplicates );
178 $this->css_rules[ $duplicate_selectors ] = new WP_Style_Engine_CSS_Rule( $duplicate_selectors, $declarations );
179 }
180 }
181}
182