stylesheet.php 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. <?php
  2. namespace Elementor;
  3. if ( ! defined( 'ABSPATH' ) ) {
  4. exit; // Exit if accessed directly.
  5. }
  6. /**
  7. * Elementor stylesheet.
  8. *
  9. * Elementor stylesheet handler class responsible for setting up CSS rules and
  10. * properties, and all the CSS `@media` rule with supported viewport width.
  11. *
  12. * @since 1.0.0
  13. */
  14. class Stylesheet {
  15. /**
  16. * CSS Rules.
  17. *
  18. * Holds the list of CSS rules.
  19. *
  20. * @since 1.0.0
  21. * @access private
  22. *
  23. * @var array A list of CSS rules.
  24. */
  25. private $rules = [];
  26. /**
  27. * Devices.
  28. *
  29. * Holds the list of devices.
  30. *
  31. * @since 1.0.0
  32. * @access private
  33. *
  34. * @var array A list of devices.
  35. */
  36. private $devices = [];
  37. /**
  38. * Raw CSS.
  39. *
  40. * Holds the raw CSS.
  41. *
  42. * @since 1.0.0
  43. * @access private
  44. *
  45. * @var array The raw CSS.
  46. */
  47. private $raw = [];
  48. /**
  49. * Parse CSS rules.
  50. *
  51. * Goes over the list of CSS rules and generates the final CSS.
  52. *
  53. * @since 1.0.0
  54. * @access public
  55. * @static
  56. *
  57. * @param array $rules CSS rules.
  58. *
  59. * @return string Parsed rules.
  60. */
  61. public static function parse_rules( array $rules ) {
  62. $parsed_rules = '';
  63. foreach ( $rules as $selector => $properties ) {
  64. $selector_content = self::parse_properties( $properties );
  65. if ( $selector_content ) {
  66. $parsed_rules .= $selector . '{' . $selector_content . '}';
  67. }
  68. }
  69. return $parsed_rules;
  70. }
  71. /**
  72. * Parse CSS properties.
  73. *
  74. * Goes over the selector properties and generates the CSS of the selector.
  75. *
  76. * @since 1.0.0
  77. * @access public
  78. * @static
  79. *
  80. * @param array $properties CSS properties.
  81. *
  82. * @return string Parsed properties.
  83. */
  84. public static function parse_properties( array $properties ) {
  85. $parsed_properties = '';
  86. foreach ( $properties as $property_key => $property_value ) {
  87. if ( '' !== $property_value ) {
  88. $parsed_properties .= $property_key . ':' . $property_value . ';';
  89. }
  90. }
  91. return $parsed_properties;
  92. }
  93. /**
  94. * Add device.
  95. *
  96. * Add a new device to the devices list.
  97. *
  98. * @since 1.0.0
  99. * @access public
  100. *
  101. * @param string $device_name Device name.
  102. * @param string $device_max_point Device maximum point.
  103. *
  104. * @return Stylesheet The current stylesheet class instance.
  105. */
  106. public function add_device( $device_name, $device_max_point ) {
  107. $this->devices[ $device_name ] = $device_max_point;
  108. asort( $this->devices );
  109. return $this;
  110. }
  111. /**
  112. * Add rules.
  113. *
  114. * Add a new CSS rule to the rules list.
  115. *
  116. * @since 1.0.0
  117. * @access public
  118. *
  119. * @param string $selector CSS selector.
  120. * @param array|string $style_rules Optional. Style rules. Default is `null`.
  121. * @param array $query Optional. Media query. Default is `null`.
  122. *
  123. * @return Stylesheet The current stylesheet class instance.
  124. */
  125. public function add_rules( $selector, $style_rules = null, array $query = null ) {
  126. $query_hash = 'all';
  127. if ( $query ) {
  128. $query_hash = $this->query_to_hash( $query );
  129. }
  130. if ( ! isset( $this->rules[ $query_hash ] ) ) {
  131. $this->add_query_hash( $query_hash );
  132. }
  133. if ( null === $style_rules ) {
  134. preg_match_all( '/([^\s].+?(?=\{))\{((?s:.)+?(?=}))}/', $selector, $parsed_rules );
  135. foreach ( $parsed_rules[1] as $index => $selector ) {
  136. $this->add_rules( $selector, $parsed_rules[2][ $index ], $query );
  137. }
  138. return $this;
  139. }
  140. if ( ! isset( $this->rules[ $query_hash ][ $selector ] ) ) {
  141. $this->rules[ $query_hash ][ $selector ] = [];
  142. }
  143. if ( is_string( $style_rules ) ) {
  144. $style_rules = array_filter( explode( ';', trim( $style_rules ) ) );
  145. $ordered_rules = [];
  146. foreach ( $style_rules as $rule ) {
  147. $property = explode( ':', $rule, 2 );
  148. if ( count( $property ) < 2 ) {
  149. return $this;
  150. }
  151. $ordered_rules[ trim( $property[0] ) ] = trim( $property[1], ' ;' );
  152. }
  153. $style_rules = $ordered_rules;
  154. }
  155. $this->rules[ $query_hash ][ $selector ] = array_merge( $this->rules[ $query_hash ][ $selector ], $style_rules );
  156. return $this;
  157. }
  158. /**
  159. * Add raw CSS.
  160. *
  161. * Add a raw CSS rule.
  162. *
  163. * @since 1.0.8
  164. * @access public
  165. *
  166. * @param string $css The raw CSS.
  167. * @param string $device Optional. The device. Default is empty.
  168. *
  169. * @return Stylesheet The current stylesheet class instance.
  170. */
  171. public function add_raw_css( $css, $device = '' ) {
  172. if ( ! isset( $this->raw[ $device ] ) ) {
  173. $this->raw[ $device ] = [];
  174. }
  175. $this->raw[ $device ][] = trim( $css );
  176. return $this;
  177. }
  178. /**
  179. * Get CSS rules.
  180. *
  181. * Retrieve the CSS rules.
  182. *
  183. * @since 1.0.5
  184. * @access public
  185. *
  186. * @param string $device Optional. The device. Default is empty.
  187. * @param string $selector Optional. CSS selector. Default is empty.
  188. * @param string $property Optional. CSS property. Default is empty.
  189. *
  190. * @return null|array CSS rules, or `null` if not rules found.
  191. */
  192. public function get_rules( $device = null, $selector = null, $property = null ) {
  193. if ( ! $device ) {
  194. return $this->rules;
  195. }
  196. if ( $property ) {
  197. return isset( $this->rules[ $device ][ $selector ][ $property ] ) ? $this->rules[ $device ][ $selector ][ $property ] : null;
  198. }
  199. if ( $selector ) {
  200. return isset( $this->rules[ $device ][ $selector ] ) ? $this->rules[ $device ][ $selector ] : null;
  201. }
  202. return isset( $this->rules[ $device ] ) ? $this->rules[ $device ] : null;
  203. }
  204. /**
  205. * To string.
  206. *
  207. * This magic method responsible for parsing the rules into one CSS string.
  208. *
  209. * @since 1.0.0
  210. * @access public
  211. *
  212. * @return string CSS style.
  213. */
  214. public function __toString() {
  215. $style_text = '';
  216. foreach ( $this->rules as $query_hash => $rule ) {
  217. $device_text = self::parse_rules( $rule );
  218. if ( 'all' !== $query_hash ) {
  219. $device_text = $this->get_query_hash_style_format( $query_hash ) . '{' . $device_text . '}';
  220. }
  221. $style_text .= $device_text;
  222. }
  223. foreach ( $this->raw as $device_name => $raw ) {
  224. $raw = implode( "\n", $raw );
  225. if ( $raw && isset( $this->devices[ $device_name ] ) ) {
  226. $raw = '@media(max-width: ' . $this->devices[ $device_name ] . 'px){' . $raw . '}';
  227. }
  228. $style_text .= $raw;
  229. }
  230. return $style_text;
  231. }
  232. /**
  233. * Get device maximum value.
  234. *
  235. * Retrieve the maximum size of any given device.
  236. *
  237. * @since 1.2.0
  238. * @access private
  239. *
  240. * @throws \RangeException If max value for this device is out of range.
  241. *
  242. * @param string $device_name Device name.
  243. *
  244. * @return int
  245. */
  246. private function get_device_max_value( $device_name ) {
  247. $devices_names = array_keys( $this->devices );
  248. $device_name_index = array_search( $device_name, $devices_names );
  249. $next_index = $device_name_index + 1;
  250. if ( $next_index >= count( $devices_names ) ) {
  251. throw new \RangeException( 'Max value for this device is out of range.' );
  252. }
  253. return $this->devices[ $devices_names[ $next_index ] ] - 1;
  254. }
  255. /**
  256. * Query to hash.
  257. *
  258. * Turns the media query into a hashed string that represents the query
  259. * endpoint in the rules list.
  260. *
  261. * @since 1.2.0
  262. * @access private
  263. *
  264. * @param array $query CSS media query.
  265. *
  266. * @return string Hashed string of the query.
  267. */
  268. private function query_to_hash( array $query ) {
  269. $hash = [];
  270. foreach ( $query as $endpoint => $value ) {
  271. $hash[] = $endpoint . '_' . $value;
  272. }
  273. return implode( '-', $hash );
  274. }
  275. /**
  276. * Hash to query.
  277. *
  278. * Turns the hashed string to an array that contains the data of the query
  279. * endpoint.
  280. *
  281. * @since 1.2.0
  282. * @access private
  283. *
  284. * @param string $hash Hashed string of the query.
  285. *
  286. * @return array Media query data.
  287. */
  288. private function hash_to_query( $hash ) {
  289. $query = [];
  290. $hash = array_filter( explode( '-', $hash ) );
  291. foreach ( $hash as $single_query ) {
  292. $query_parts = explode( '_', $single_query );
  293. $end_point = $query_parts[0];
  294. $device_name = $query_parts[1];
  295. $query[ $end_point ] = 'max' === $end_point ? $this->get_device_max_value( $device_name ) : $this->devices[ $device_name ];
  296. }
  297. return $query;
  298. }
  299. /**
  300. * Add query hash.
  301. *
  302. * Register new endpoint query and sort the rules the way they should be
  303. * displayed in the final stylesheet based on the device and the viewport
  304. * width.
  305. *
  306. * @since 1.2.0
  307. * @access private
  308. *
  309. * @param string $query_hash Hashed string of the query.
  310. */
  311. private function add_query_hash( $query_hash ) {
  312. $this->rules[ $query_hash ] = [];
  313. uksort(
  314. $this->rules, function( $a, $b ) {
  315. if ( 'all' === $a ) {
  316. return -1;
  317. }
  318. if ( 'all' === $b ) {
  319. return 1;
  320. }
  321. $a_query = $this->hash_to_query( $a );
  322. $b_query = $this->hash_to_query( $b );
  323. if ( isset( $a_query['min'] ) xor isset( $b_query['min'] ) ) {
  324. return 1;
  325. }
  326. if ( isset( $a_query['min'] ) ) {
  327. $range = $a_query['min'] - $b_query['min'];
  328. if ( $range ) {
  329. return $range;
  330. }
  331. $a_has_max = isset( $a_query['max'] );
  332. if ( $a_has_max xor isset( $b_query['max'] ) ) {
  333. return $a_has_max ? 1 : -1;
  334. }
  335. if ( ! $a_has_max ) {
  336. return 0;
  337. }
  338. }
  339. return $b_query['max'] - $a_query['max'];
  340. }
  341. );
  342. }
  343. /**
  344. * Get query hash style format.
  345. *
  346. * Retrieve formated media query rule with the endpoint width settings.
  347. *
  348. * The method returns the CSS `@media` rule and supported viewport width in
  349. * pixels. It can also handel multiple width endpoints.
  350. *
  351. * @since 1.2.0
  352. * @access private
  353. *
  354. * @param string $query_hash The hash of the query.
  355. *
  356. * @return string CSS media query.
  357. */
  358. private function get_query_hash_style_format( $query_hash ) {
  359. $query = $this->hash_to_query( $query_hash );
  360. $style_format = [];
  361. foreach ( $query as $end_point => $value ) {
  362. $style_format[] = '(' . $end_point . '-width:' . $value . 'px)';
  363. }
  364. return '@media' . implode( ' and ', $style_format );
  365. }
  366. }