Nenhuma Descrição

class-wp-theme-json.php 33KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238
  1. <?php
  2. /**
  3. * WP_Theme_JSON class
  4. *
  5. * @package WordPress
  6. * @subpackage Theme
  7. * @since 5.8.0
  8. */
  9. /**
  10. * Class that encapsulates the processing of structures that adhere to the theme.json spec.
  11. *
  12. * @access private
  13. */
  14. class WP_Theme_JSON {
  15. /**
  16. * Container of data in theme.json format.
  17. *
  18. * @since 5.8.0
  19. * @var array
  20. */
  21. private $theme_json = null;
  22. /**
  23. * Holds block metadata extracted from block.json
  24. * to be shared among all instances so we don't
  25. * process it twice.
  26. *
  27. * @since 5.8.0
  28. * @var array
  29. */
  30. private static $blocks_metadata = null;
  31. /**
  32. * The CSS selector for the top-level styles.
  33. *
  34. * @since 5.8.0
  35. * @var string
  36. */
  37. const ROOT_BLOCK_SELECTOR = 'body';
  38. /**
  39. * The sources of data this object can represent.
  40. *
  41. * @since 5.8.0
  42. * @var array
  43. */
  44. const VALID_ORIGINS = array(
  45. 'core',
  46. 'theme',
  47. 'user',
  48. );
  49. /**
  50. * Presets are a set of values that serve
  51. * to bootstrap some styles: colors, font sizes, etc.
  52. *
  53. * They are a unkeyed array of values such as:
  54. *
  55. * ```php
  56. * array(
  57. * array(
  58. * 'slug' => 'unique-name-within-the-set',
  59. * 'name' => 'Name for the UI',
  60. * <value_key> => 'value'
  61. * ),
  62. * )
  63. * ```
  64. *
  65. * This contains the necessary metadata to process them:
  66. *
  67. * - path => where to find the preset within the settings section
  68. *
  69. * - value_key => the key that represents the value
  70. *
  71. * - css_var_infix => infix to use in generating the CSS Custom Property. Example:
  72. * --wp--preset--<preset_infix>--<slug>: <preset_value>
  73. *
  74. * - classes => array containing a structure with the classes to
  75. * generate for the presets. Each class should have
  76. * the class suffix and the property name. Example:
  77. *
  78. * .has-<slug>-<class_suffix> {
  79. * <property_name>: <preset_value>
  80. * }
  81. *
  82. * @since 5.8.0
  83. * @var array
  84. */
  85. const PRESETS_METADATA = array(
  86. array(
  87. 'path' => array( 'color', 'palette' ),
  88. 'value_key' => 'color',
  89. 'css_var_infix' => 'color',
  90. 'classes' => array(
  91. array(
  92. 'class_suffix' => 'color',
  93. 'property_name' => 'color',
  94. ),
  95. array(
  96. 'class_suffix' => 'background-color',
  97. 'property_name' => 'background-color',
  98. ),
  99. ),
  100. ),
  101. array(
  102. 'path' => array( 'color', 'gradients' ),
  103. 'value_key' => 'gradient',
  104. 'css_var_infix' => 'gradient',
  105. 'classes' => array(
  106. array(
  107. 'class_suffix' => 'gradient-background',
  108. 'property_name' => 'background',
  109. ),
  110. ),
  111. ),
  112. array(
  113. 'path' => array( 'typography', 'fontSizes' ),
  114. 'value_key' => 'size',
  115. 'css_var_infix' => 'font-size',
  116. 'classes' => array(
  117. array(
  118. 'class_suffix' => 'font-size',
  119. 'property_name' => 'font-size',
  120. ),
  121. ),
  122. ),
  123. );
  124. /**
  125. * Metadata for style properties.
  126. *
  127. * Each property declares:
  128. *
  129. * - 'value': path to the value in theme.json and block attributes.
  130. *
  131. * @since 5.8.0
  132. * @var array
  133. */
  134. const PROPERTIES_METADATA = array(
  135. 'background' => array(
  136. 'value' => array( 'color', 'gradient' ),
  137. ),
  138. 'background-color' => array(
  139. 'value' => array( 'color', 'background' ),
  140. ),
  141. 'color' => array(
  142. 'value' => array( 'color', 'text' ),
  143. ),
  144. 'font-size' => array(
  145. 'value' => array( 'typography', 'fontSize' ),
  146. ),
  147. 'line-height' => array(
  148. 'value' => array( 'typography', 'lineHeight' ),
  149. ),
  150. 'margin' => array(
  151. 'value' => array( 'spacing', 'margin' ),
  152. 'properties' => array( 'top', 'right', 'bottom', 'left' ),
  153. ),
  154. 'padding' => array(
  155. 'value' => array( 'spacing', 'padding' ),
  156. 'properties' => array( 'top', 'right', 'bottom', 'left' ),
  157. ),
  158. );
  159. /**
  160. * @since 5.8.0
  161. * @var array
  162. */
  163. const ALLOWED_TOP_LEVEL_KEYS = array(
  164. 'settings',
  165. 'styles',
  166. 'version',
  167. );
  168. /**
  169. * @since 5.8.0
  170. * @var array
  171. */
  172. const ALLOWED_SETTINGS = array(
  173. 'border' => array(
  174. 'customRadius' => null,
  175. ),
  176. 'color' => array(
  177. 'custom' => null,
  178. 'customDuotone' => null,
  179. 'customGradient' => null,
  180. 'duotone' => null,
  181. 'gradients' => null,
  182. 'link' => null,
  183. 'palette' => null,
  184. ),
  185. 'custom' => null,
  186. 'layout' => array(
  187. 'contentSize' => null,
  188. 'wideSize' => null,
  189. ),
  190. 'spacing' => array(
  191. 'customMargin' => null,
  192. 'customPadding' => null,
  193. 'units' => null,
  194. ),
  195. 'typography' => array(
  196. 'customFontSize' => null,
  197. 'customLineHeight' => null,
  198. 'dropCap' => null,
  199. 'fontSizes' => null,
  200. ),
  201. );
  202. /**
  203. * @since 5.8.0
  204. * @var array
  205. */
  206. const ALLOWED_STYLES = array(
  207. 'border' => array(
  208. 'radius' => null,
  209. ),
  210. 'color' => array(
  211. 'background' => null,
  212. 'gradient' => null,
  213. 'text' => null,
  214. ),
  215. 'spacing' => array(
  216. 'margin' => array(
  217. 'top' => null,
  218. 'right' => null,
  219. 'bottom' => null,
  220. 'left' => null,
  221. ),
  222. 'padding' => array(
  223. 'bottom' => null,
  224. 'left' => null,
  225. 'right' => null,
  226. 'top' => null,
  227. ),
  228. ),
  229. 'typography' => array(
  230. 'fontSize' => null,
  231. 'lineHeight' => null,
  232. ),
  233. );
  234. /**
  235. * @since 5.8.0
  236. * @var array
  237. */
  238. const ELEMENTS = array(
  239. 'link' => 'a',
  240. 'h1' => 'h1',
  241. 'h2' => 'h2',
  242. 'h3' => 'h3',
  243. 'h4' => 'h4',
  244. 'h5' => 'h5',
  245. 'h6' => 'h6',
  246. );
  247. /**
  248. * @since 5.8.0
  249. * @var int
  250. */
  251. const LATEST_SCHEMA = 1;
  252. /**
  253. * Constructor.
  254. *
  255. * @since 5.8.0
  256. *
  257. * @param array $theme_json A structure that follows the theme.json schema.
  258. * @param string $origin Optional. What source of data this object represents.
  259. * One of 'core', 'theme', or 'user'. Default 'theme'.
  260. */
  261. public function __construct( $theme_json = array(), $origin = 'theme' ) {
  262. if ( ! in_array( $origin, self::VALID_ORIGINS, true ) ) {
  263. $origin = 'theme';
  264. }
  265. if ( ! isset( $theme_json['version'] ) || self::LATEST_SCHEMA !== $theme_json['version'] ) {
  266. $this->theme_json = array();
  267. return;
  268. }
  269. $this->theme_json = self::sanitize( $theme_json );
  270. // Internally, presets are keyed by origin.
  271. $nodes = self::get_setting_nodes( $this->theme_json );
  272. foreach ( $nodes as $node ) {
  273. foreach ( self::PRESETS_METADATA as $preset ) {
  274. $path = array_merge( $node['path'], $preset['path'] );
  275. $preset = _wp_array_get( $this->theme_json, $path, null );
  276. if ( null !== $preset ) {
  277. _wp_array_set( $this->theme_json, $path, array( $origin => $preset ) );
  278. }
  279. }
  280. }
  281. }
  282. /**
  283. * Sanitizes the input according to the schemas.
  284. *
  285. * @since 5.8.0
  286. *
  287. * @param array $input Structure to sanitize.
  288. * @return array The sanitized output.
  289. */
  290. private static function sanitize( $input ) {
  291. $output = array();
  292. if ( ! is_array( $input ) ) {
  293. return $output;
  294. }
  295. $allowed_top_level_keys = self::ALLOWED_TOP_LEVEL_KEYS;
  296. $allowed_settings = self::ALLOWED_SETTINGS;
  297. $allowed_styles = self::ALLOWED_STYLES;
  298. $allowed_blocks = array_keys( self::get_blocks_metadata() );
  299. $allowed_elements = array_keys( self::ELEMENTS );
  300. $output = array_intersect_key( $input, array_flip( $allowed_top_level_keys ) );
  301. // Build the schema.
  302. $schema = array();
  303. $schema_styles_elements = array();
  304. foreach ( $allowed_elements as $element ) {
  305. $schema_styles_elements[ $element ] = $allowed_styles;
  306. }
  307. $schema_styles_blocks = array();
  308. $schema_settings_blocks = array();
  309. foreach ( $allowed_blocks as $block ) {
  310. $schema_settings_blocks[ $block ] = $allowed_settings;
  311. $schema_styles_blocks[ $block ] = $allowed_styles;
  312. $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
  313. }
  314. $schema['styles'] = $allowed_styles;
  315. $schema['styles']['blocks'] = $schema_styles_blocks;
  316. $schema['styles']['elements'] = $schema_styles_elements;
  317. $schema['settings'] = $allowed_settings;
  318. $schema['settings']['blocks'] = $schema_settings_blocks;
  319. // Remove anything that's not present in the schema.
  320. foreach ( array( 'styles', 'settings' ) as $subtree ) {
  321. if ( ! isset( $input[ $subtree ] ) ) {
  322. continue;
  323. }
  324. if ( ! is_array( $input[ $subtree ] ) ) {
  325. unset( $output[ $subtree ] );
  326. continue;
  327. }
  328. $result = self::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );
  329. if ( empty( $result ) ) {
  330. unset( $output[ $subtree ] );
  331. } else {
  332. $output[ $subtree ] = $result;
  333. }
  334. }
  335. return $output;
  336. }
  337. /**
  338. * Returns the metadata for each block.
  339. *
  340. * Example:
  341. *
  342. * {
  343. * 'core/paragraph': {
  344. * 'selector': 'p',
  345. * 'elements': {
  346. * 'link' => 'link selector',
  347. * 'etc' => 'element selector'
  348. * }
  349. * },
  350. * 'core/heading': {
  351. * 'selector': 'h1',
  352. * 'elements': {}
  353. * }
  354. * 'core/group': {
  355. * 'selector': '.wp-block-group',
  356. * 'elements': {}
  357. * }
  358. * }
  359. *
  360. * @since 5.8.0
  361. *
  362. * @return array Block metadata.
  363. */
  364. private static function get_blocks_metadata() {
  365. if ( null !== self::$blocks_metadata ) {
  366. return self::$blocks_metadata;
  367. }
  368. self::$blocks_metadata = array();
  369. $registry = WP_Block_Type_Registry::get_instance();
  370. $blocks = $registry->get_all_registered();
  371. foreach ( $blocks as $block_name => $block_type ) {
  372. if (
  373. isset( $block_type->supports['__experimentalSelector'] ) &&
  374. is_string( $block_type->supports['__experimentalSelector'] )
  375. ) {
  376. self::$blocks_metadata[ $block_name ]['selector'] = $block_type->supports['__experimentalSelector'];
  377. } else {
  378. self::$blocks_metadata[ $block_name ]['selector'] = '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) );
  379. }
  380. /*
  381. * Assign defaults, then overwrite those that the block sets by itself.
  382. * If the block selector is compounded, will append the element to each
  383. * individual block selector.
  384. */
  385. $block_selectors = explode( ',', self::$blocks_metadata[ $block_name ]['selector'] );
  386. foreach ( self::ELEMENTS as $el_name => $el_selector ) {
  387. $element_selector = array();
  388. foreach ( $block_selectors as $selector ) {
  389. $element_selector[] = $selector . ' ' . $el_selector;
  390. }
  391. self::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector );
  392. }
  393. }
  394. return self::$blocks_metadata;
  395. }
  396. /**
  397. * Given a tree, removes the keys that are not present in the schema.
  398. *
  399. * It is recursive and modifies the input in-place.
  400. *
  401. * @since 5.8.0
  402. *
  403. * @param array $tree Input to process.
  404. * @param array $schema Schema to adhere to.
  405. * @return array Returns the modified $tree.
  406. */
  407. private static function remove_keys_not_in_schema( $tree, $schema ) {
  408. $tree = array_intersect_key( $tree, $schema );
  409. foreach ( $schema as $key => $data ) {
  410. if ( ! isset( $tree[ $key ] ) ) {
  411. continue;
  412. }
  413. if ( is_array( $schema[ $key ] ) && is_array( $tree[ $key ] ) ) {
  414. $tree[ $key ] = self::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] );
  415. if ( empty( $tree[ $key ] ) ) {
  416. unset( $tree[ $key ] );
  417. }
  418. } elseif ( is_array( $schema[ $key ] ) && ! is_array( $tree[ $key ] ) ) {
  419. unset( $tree[ $key ] );
  420. }
  421. }
  422. return $tree;
  423. }
  424. /**
  425. * Returns the existing settings for each block.
  426. *
  427. * Example:
  428. *
  429. * {
  430. * 'root': {
  431. * 'color': {
  432. * 'custom': true
  433. * }
  434. * },
  435. * 'core/paragraph': {
  436. * 'spacing': {
  437. * 'customPadding': true
  438. * }
  439. * }
  440. * }
  441. *
  442. * @since 5.8.0
  443. *
  444. * @return array Settings per block.
  445. */
  446. public function get_settings() {
  447. if ( ! isset( $this->theme_json['settings'] ) ) {
  448. return array();
  449. } else {
  450. return $this->theme_json['settings'];
  451. }
  452. }
  453. /**
  454. * Returns the stylesheet that results of processing
  455. * the theme.json structure this object represents.
  456. *
  457. * @since 5.8.0
  458. *
  459. * @param string $type Optional. Type of stylesheet we want. Accepts 'all',
  460. * 'block_styles', and 'css_variables'. Default 'all'.
  461. * @return string Stylesheet.
  462. */
  463. public function get_stylesheet( $type = 'all' ) {
  464. $blocks_metadata = self::get_blocks_metadata();
  465. $style_nodes = self::get_style_nodes( $this->theme_json, $blocks_metadata );
  466. $setting_nodes = self::get_setting_nodes( $this->theme_json, $blocks_metadata );
  467. switch ( $type ) {
  468. case 'block_styles':
  469. return $this->get_block_styles( $style_nodes, $setting_nodes );
  470. case 'css_variables':
  471. return $this->get_css_variables( $setting_nodes );
  472. default:
  473. return $this->get_css_variables( $setting_nodes ) . $this->get_block_styles( $style_nodes, $setting_nodes );
  474. }
  475. }
  476. /**
  477. * Converts each style section into a list of rulesets
  478. * containing the block styles to be appended to the stylesheet.
  479. *
  480. * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
  481. *
  482. * For each section this creates a new ruleset such as:
  483. *
  484. * block-selector {
  485. * style-property-one: value;
  486. * }
  487. *
  488. * Additionally, it'll also create new rulesets
  489. * as classes for each preset value such as:
  490. *
  491. * .has-value-color {
  492. * color: value;
  493. * }
  494. *
  495. * .has-value-background-color {
  496. * background-color: value;
  497. * }
  498. *
  499. * .has-value-font-size {
  500. * font-size: value;
  501. * }
  502. *
  503. * .has-value-gradient-background {
  504. * background: value;
  505. * }
  506. *
  507. * p.has-value-gradient-background {
  508. * background: value;
  509. * }
  510. *
  511. * @since 5.8.0
  512. *
  513. * @param array $style_nodes Nodes with styles.
  514. * @param array $setting_nodes Nodes with settings.
  515. * @return string The new stylesheet.
  516. */
  517. private function get_block_styles( $style_nodes, $setting_nodes ) {
  518. $block_rules = '';
  519. foreach ( $style_nodes as $metadata ) {
  520. if ( null === $metadata['selector'] ) {
  521. continue;
  522. }
  523. $node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
  524. $selector = $metadata['selector'];
  525. $declarations = self::compute_style_properties( $node );
  526. $block_rules .= self::to_ruleset( $selector, $declarations );
  527. }
  528. $preset_rules = '';
  529. foreach ( $setting_nodes as $metadata ) {
  530. if ( null === $metadata['selector'] ) {
  531. continue;
  532. }
  533. $selector = $metadata['selector'];
  534. $node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
  535. $preset_rules .= self::compute_preset_classes( $node, $selector );
  536. }
  537. return $block_rules . $preset_rules;
  538. }
  539. /**
  540. * Converts each styles section into a list of rulesets
  541. * to be appended to the stylesheet.
  542. * These rulesets contain all the css variables (custom variables and preset variables).
  543. *
  544. * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
  545. *
  546. * For each section this creates a new ruleset such as:
  547. *
  548. * block-selector {
  549. * --wp--preset--category--slug: value;
  550. * --wp--custom--variable: value;
  551. * }
  552. *
  553. * @since 5.8.0
  554. *
  555. * @param array $nodes Nodes with settings.
  556. * @return string The new stylesheet.
  557. */
  558. private function get_css_variables( $nodes ) {
  559. $stylesheet = '';
  560. foreach ( $nodes as $metadata ) {
  561. if ( null === $metadata['selector'] ) {
  562. continue;
  563. }
  564. $selector = $metadata['selector'];
  565. $node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
  566. $declarations = array_merge( self::compute_preset_vars( $node ), self::compute_theme_vars( $node ) );
  567. $stylesheet .= self::to_ruleset( $selector, $declarations );
  568. }
  569. return $stylesheet;
  570. }
  571. /**
  572. * Given a selector and a declaration list,
  573. * creates the corresponding ruleset.
  574. *
  575. * @since 5.8.0
  576. *
  577. * @param string $selector CSS selector.
  578. * @param array $declarations List of declarations.
  579. * @return string CSS ruleset.
  580. */
  581. private static function to_ruleset( $selector, $declarations ) {
  582. if ( empty( $declarations ) ) {
  583. return '';
  584. }
  585. $declaration_block = array_reduce(
  586. $declarations,
  587. function ( $carry, $element ) {
  588. return $carry .= $element['name'] . ': ' . $element['value'] . ';'; },
  589. ''
  590. );
  591. return $selector . '{' . $declaration_block . '}';
  592. }
  593. /**
  594. * Function that appends a sub-selector to a existing one.
  595. *
  596. * Given the compounded $selector "h1, h2, h3"
  597. * and the $to_append selector ".some-class" the result will be
  598. * "h1.some-class, h2.some-class, h3.some-class".
  599. *
  600. * @since 5.8.0
  601. *
  602. * @param string $selector Original selector.
  603. * @param string $to_append Selector to append.
  604. * @return string
  605. */
  606. private static function append_to_selector( $selector, $to_append ) {
  607. $new_selectors = array();
  608. $selectors = explode( ',', $selector );
  609. foreach ( $selectors as $sel ) {
  610. $new_selectors[] = $sel . $to_append;
  611. }
  612. return implode( ',', $new_selectors );
  613. }
  614. /**
  615. * Given an array of presets keyed by origin and the value key of the preset,
  616. * it returns an array where each key is the preset slug and each value the preset value.
  617. *
  618. * @since 5.8.0
  619. *
  620. * @param array $preset_per_origin Array of presets keyed by origin.
  621. * @param string $value_key The property of the preset that contains its value.
  622. * @return array Array of presets where each key is a slug and each value is the preset value.
  623. */
  624. private static function get_merged_preset_by_slug( $preset_per_origin, $value_key ) {
  625. $result = array();
  626. foreach ( self::VALID_ORIGINS as $origin ) {
  627. if ( ! isset( $preset_per_origin[ $origin ] ) ) {
  628. continue;
  629. }
  630. foreach ( $preset_per_origin[ $origin ] as $preset ) {
  631. /*
  632. * We don't want to use kebabCase here,
  633. * see https://github.com/WordPress/gutenberg/issues/32347
  634. * However, we need to make sure the generated class or CSS variable
  635. * doesn't contain spaces.
  636. */
  637. $result[ preg_replace( '/\s+/', '-', $preset['slug'] ) ] = $preset[ $value_key ];
  638. }
  639. }
  640. return $result;
  641. }
  642. /**
  643. * Given a settings array, it returns the generated rulesets
  644. * for the preset classes.
  645. *
  646. * @since 5.8.0
  647. *
  648. * @param array $settings Settings to process.
  649. * @param string $selector Selector wrapping the classes.
  650. * @return string The result of processing the presets.
  651. */
  652. private static function compute_preset_classes( $settings, $selector ) {
  653. if ( self::ROOT_BLOCK_SELECTOR === $selector ) {
  654. // Classes at the global level do not need any CSS prefixed,
  655. // and we don't want to increase its specificity.
  656. $selector = '';
  657. }
  658. $stylesheet = '';
  659. foreach ( self::PRESETS_METADATA as $preset ) {
  660. $preset_per_origin = _wp_array_get( $settings, $preset['path'], array() );
  661. $preset_by_slug = self::get_merged_preset_by_slug( $preset_per_origin, $preset['value_key'] );
  662. foreach ( $preset['classes'] as $class ) {
  663. foreach ( $preset_by_slug as $slug => $value ) {
  664. $stylesheet .= self::to_ruleset(
  665. self::append_to_selector( $selector, '.has-' . _wp_to_kebab_case( $slug ) . '-' . $class['class_suffix'] ),
  666. array(
  667. array(
  668. 'name' => $class['property_name'],
  669. 'value' => 'var(--wp--preset--' . $preset['css_var_infix'] . '--' . _wp_to_kebab_case( $slug ) . ') !important',
  670. ),
  671. )
  672. );
  673. }
  674. }
  675. }
  676. return $stylesheet;
  677. }
  678. /**
  679. * Given the block settings, it extracts the CSS Custom Properties
  680. * for the presets and adds them to the $declarations array
  681. * following the format:
  682. *
  683. * array(
  684. * 'name' => 'property_name',
  685. * 'value' => 'property_value,
  686. * )
  687. *
  688. * @since 5.8.0
  689. *
  690. * @param array $settings Settings to process.
  691. * @return array Returns the modified $declarations.
  692. */
  693. private static function compute_preset_vars( $settings ) {
  694. $declarations = array();
  695. foreach ( self::PRESETS_METADATA as $preset ) {
  696. $preset_per_origin = _wp_array_get( $settings, $preset['path'], array() );
  697. $preset_by_slug = self::get_merged_preset_by_slug( $preset_per_origin, $preset['value_key'] );
  698. foreach ( $preset_by_slug as $slug => $value ) {
  699. $declarations[] = array(
  700. 'name' => '--wp--preset--' . $preset['css_var_infix'] . '--' . _wp_to_kebab_case( $slug ),
  701. 'value' => $value,
  702. );
  703. }
  704. }
  705. return $declarations;
  706. }
  707. /**
  708. * Given an array of settings, it extracts the CSS Custom Properties
  709. * for the custom values and adds them to the $declarations
  710. * array following the format:
  711. *
  712. * array(
  713. * 'name' => 'property_name',
  714. * 'value' => 'property_value,
  715. * )
  716. *
  717. * @since 5.8.0
  718. *
  719. * @param array $settings Settings to process.
  720. * @return array Returns the modified $declarations.
  721. */
  722. private static function compute_theme_vars( $settings ) {
  723. $declarations = array();
  724. $custom_values = _wp_array_get( $settings, array( 'custom' ), array() );
  725. $css_vars = self::flatten_tree( $custom_values );
  726. foreach ( $css_vars as $key => $value ) {
  727. $declarations[] = array(
  728. 'name' => '--wp--custom--' . $key,
  729. 'value' => $value,
  730. );
  731. }
  732. return $declarations;
  733. }
  734. /**
  735. * Given a tree, it creates a flattened one
  736. * by merging the keys and binding the leaf values
  737. * to the new keys.
  738. *
  739. * It also transforms camelCase names into kebab-case
  740. * and substitutes '/' by '-'.
  741. *
  742. * This is thought to be useful to generate
  743. * CSS Custom Properties from a tree,
  744. * although there's nothing in the implementation
  745. * of this function that requires that format.
  746. *
  747. * For example, assuming the given prefix is '--wp'
  748. * and the token is '--', for this input tree:
  749. *
  750. * {
  751. * 'some/property': 'value',
  752. * 'nestedProperty': {
  753. * 'sub-property': 'value'
  754. * }
  755. * }
  756. *
  757. * it'll return this output:
  758. *
  759. * {
  760. * '--wp--some-property': 'value',
  761. * '--wp--nested-property--sub-property': 'value'
  762. * }
  763. *
  764. * @since 5.8.0
  765. *
  766. * @param array $tree Input tree to process.
  767. * @param string $prefix Optional. Prefix to prepend to each variable. Default empty string.
  768. * @param string $token Optional. Token to use between levels. Default '--'.
  769. * @return array The flattened tree.
  770. */
  771. private static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
  772. $result = array();
  773. foreach ( $tree as $property => $value ) {
  774. $new_key = $prefix . str_replace(
  775. '/',
  776. '-',
  777. strtolower( preg_replace( '/(?<!^)[A-Z]/', '-$0', $property ) ) // CamelCase to kebab-case.
  778. );
  779. if ( is_array( $value ) ) {
  780. $new_prefix = $new_key . $token;
  781. $result = array_merge(
  782. $result,
  783. self::flatten_tree( $value, $new_prefix, $token )
  784. );
  785. } else {
  786. $result[ $new_key ] = $value;
  787. }
  788. }
  789. return $result;
  790. }
  791. /**
  792. * Given a styles array, it extracts the style properties
  793. * and adds them to the $declarations array following the format:
  794. *
  795. * array(
  796. * 'name' => 'property_name',
  797. * 'value' => 'property_value,
  798. * )
  799. *
  800. * @since 5.8.0
  801. *
  802. * @param array $styles Styles to process.
  803. * @return array Returns the modified $declarations.
  804. */
  805. private static function compute_style_properties( $styles ) {
  806. $declarations = array();
  807. if ( empty( $styles ) ) {
  808. return $declarations;
  809. }
  810. $properties = array();
  811. foreach ( self::PROPERTIES_METADATA as $name => $metadata ) {
  812. /*
  813. * Some properties can be shorthand properties, meaning that
  814. * they contain multiple values instead of a single one.
  815. * An example of this is the padding property.
  816. */
  817. if ( self::has_properties( $metadata ) ) {
  818. foreach ( $metadata['properties'] as $property ) {
  819. $properties[] = array(
  820. 'name' => $name . '-' . $property,
  821. 'value' => array_merge( $metadata['value'], array( $property ) ),
  822. );
  823. }
  824. } else {
  825. $properties[] = array(
  826. 'name' => $name,
  827. 'value' => $metadata['value'],
  828. );
  829. }
  830. }
  831. foreach ( $properties as $prop ) {
  832. $value = self::get_property_value( $styles, $prop['value'] );
  833. if ( empty( $value ) ) {
  834. continue;
  835. }
  836. $declarations[] = array(
  837. 'name' => $prop['name'],
  838. 'value' => $value,
  839. );
  840. }
  841. return $declarations;
  842. }
  843. /**
  844. * Whether the metadata contains a key named properties.
  845. *
  846. * @since 5.8.0
  847. *
  848. * @param array $metadata Description of the style property.
  849. * @return bool True if properties exists, false otherwise.
  850. */
  851. private static function has_properties( $metadata ) {
  852. if ( array_key_exists( 'properties', $metadata ) ) {
  853. return true;
  854. }
  855. return false;
  856. }
  857. /**
  858. * Returns the style property for the given path.
  859. *
  860. * It also converts CSS Custom Property stored as
  861. * "var:preset|color|secondary" to the form
  862. * "--wp--preset--color--secondary".
  863. *
  864. * @since 5.8.0
  865. *
  866. * @param array $styles Styles subtree.
  867. * @param array $path Which property to process.
  868. * @return string Style property value.
  869. */
  870. private static function get_property_value( $styles, $path ) {
  871. $value = _wp_array_get( $styles, $path, '' );
  872. if ( '' === $value ) {
  873. return $value;
  874. }
  875. $prefix = 'var:';
  876. $prefix_len = strlen( $prefix );
  877. $token_in = '|';
  878. $token_out = '--';
  879. if ( 0 === strncmp( $value, $prefix, $prefix_len ) ) {
  880. $unwrapped_name = str_replace(
  881. $token_in,
  882. $token_out,
  883. substr( $value, $prefix_len )
  884. );
  885. $value = "var(--wp--$unwrapped_name)";
  886. }
  887. return $value;
  888. }
  889. /**
  890. * Builds metadata for the setting nodes, which returns in the form of:
  891. *
  892. * [
  893. * [
  894. * 'path' => ['path', 'to', 'some', 'node' ],
  895. * 'selector' => 'CSS selector for some node'
  896. * ],
  897. * [
  898. * 'path' => [ 'path', 'to', 'other', 'node' ],
  899. * 'selector' => 'CSS selector for other node'
  900. * ],
  901. * ]
  902. *
  903. * @since 5.8.0
  904. *
  905. * @param array $theme_json The tree to extract setting nodes from.
  906. * @param array $selectors List of selectors per block.
  907. * @return array
  908. */
  909. private static function get_setting_nodes( $theme_json, $selectors = array() ) {
  910. $nodes = array();
  911. if ( ! isset( $theme_json['settings'] ) ) {
  912. return $nodes;
  913. }
  914. // Top-level.
  915. $nodes[] = array(
  916. 'path' => array( 'settings' ),
  917. 'selector' => self::ROOT_BLOCK_SELECTOR,
  918. );
  919. // Calculate paths for blocks.
  920. if ( ! isset( $theme_json['settings']['blocks'] ) ) {
  921. return $nodes;
  922. }
  923. foreach ( $theme_json['settings']['blocks'] as $name => $node ) {
  924. $selector = null;
  925. if ( isset( $selectors[ $name ]['selector'] ) ) {
  926. $selector = $selectors[ $name ]['selector'];
  927. }
  928. $nodes[] = array(
  929. 'path' => array( 'settings', 'blocks', $name ),
  930. 'selector' => $selector,
  931. );
  932. }
  933. return $nodes;
  934. }
  935. /**
  936. * Builds metadata for the style nodes, which returns in the form of:
  937. *
  938. * [
  939. * [
  940. * 'path' => [ 'path', 'to', 'some', 'node' ],
  941. * 'selector' => 'CSS selector for some node'
  942. * ],
  943. * [
  944. * 'path' => ['path', 'to', 'other', 'node' ],
  945. * 'selector' => 'CSS selector for other node'
  946. * ],
  947. * ]
  948. *
  949. * @since 5.8.0
  950. *
  951. * @param array $theme_json The tree to extract style nodes from.
  952. * @param array $selectors List of selectors per block.
  953. * @return array
  954. */
  955. private static function get_style_nodes( $theme_json, $selectors = array() ) {
  956. $nodes = array();
  957. if ( ! isset( $theme_json['styles'] ) ) {
  958. return $nodes;
  959. }
  960. // Top-level.
  961. $nodes[] = array(
  962. 'path' => array( 'styles' ),
  963. 'selector' => self::ROOT_BLOCK_SELECTOR,
  964. );
  965. if ( isset( $theme_json['styles']['elements'] ) ) {
  966. foreach ( $theme_json['styles']['elements'] as $element => $node ) {
  967. $nodes[] = array(
  968. 'path' => array( 'styles', 'elements', $element ),
  969. 'selector' => self::ELEMENTS[ $element ],
  970. );
  971. }
  972. }
  973. // Blocks.
  974. if ( ! isset( $theme_json['styles']['blocks'] ) ) {
  975. return $nodes;
  976. }
  977. foreach ( $theme_json['styles']['blocks'] as $name => $node ) {
  978. $selector = null;
  979. if ( isset( $selectors[ $name ]['selector'] ) ) {
  980. $selector = $selectors[ $name ]['selector'];
  981. }
  982. $nodes[] = array(
  983. 'path' => array( 'styles', 'blocks', $name ),
  984. 'selector' => $selector,
  985. );
  986. if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
  987. foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) {
  988. $nodes[] = array(
  989. 'path' => array( 'styles', 'blocks', $name, 'elements', $element ),
  990. 'selector' => $selectors[ $name ]['elements'][ $element ],
  991. );
  992. }
  993. }
  994. }
  995. return $nodes;
  996. }
  997. /**
  998. * Merge new incoming data.
  999. *
  1000. * @since 5.8.0
  1001. *
  1002. * @param WP_Theme_JSON $incoming Data to merge.
  1003. */
  1004. public function merge( $incoming ) {
  1005. $incoming_data = $incoming->get_raw_data();
  1006. $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );
  1007. /*
  1008. * The array_replace_recursive() algorithm merges at the leaf level.
  1009. * For leaf values that are arrays it will use the numeric indexes for replacement.
  1010. * In those cases, we want to replace the existing with the incoming value, if it exists.
  1011. */
  1012. $to_replace = array();
  1013. $to_replace[] = array( 'spacing', 'units' );
  1014. $to_replace[] = array( 'color', 'duotone' );
  1015. foreach ( self::VALID_ORIGINS as $origin ) {
  1016. $to_replace[] = array( 'color', 'palette', $origin );
  1017. $to_replace[] = array( 'color', 'gradients', $origin );
  1018. $to_replace[] = array( 'typography', 'fontSizes', $origin );
  1019. $to_replace[] = array( 'typography', 'fontFamilies', $origin );
  1020. }
  1021. $nodes = self::get_setting_nodes( $this->theme_json );
  1022. foreach ( $nodes as $metadata ) {
  1023. foreach ( $to_replace as $property_path ) {
  1024. $path = array_merge( $metadata['path'], $property_path );
  1025. $node = _wp_array_get( $incoming_data, $path, null );
  1026. if ( isset( $node ) ) {
  1027. _wp_array_set( $this->theme_json, $path, $node );
  1028. }
  1029. }
  1030. }
  1031. }
  1032. /**
  1033. * Returns the raw data.
  1034. *
  1035. * @since 5.8.0
  1036. *
  1037. * @return array Raw data.
  1038. */
  1039. public function get_raw_data() {
  1040. return $this->theme_json;
  1041. }
  1042. /**
  1043. * Transforms the given editor settings according the
  1044. * add_theme_support format to the theme.json format.
  1045. *
  1046. * @since 5.8.0
  1047. *
  1048. * @param array $settings Existing editor settings.
  1049. * @return array Config that adheres to the theme.json schema.
  1050. */
  1051. public static function get_from_editor_settings( $settings ) {
  1052. $theme_settings = array(
  1053. 'version' => self::LATEST_SCHEMA,
  1054. 'settings' => array(),
  1055. );
  1056. // Deprecated theme supports.
  1057. if ( isset( $settings['disableCustomColors'] ) ) {
  1058. if ( ! isset( $theme_settings['settings']['color'] ) ) {
  1059. $theme_settings['settings']['color'] = array();
  1060. }
  1061. $theme_settings['settings']['color']['custom'] = ! $settings['disableCustomColors'];
  1062. }
  1063. if ( isset( $settings['disableCustomGradients'] ) ) {
  1064. if ( ! isset( $theme_settings['settings']['color'] ) ) {
  1065. $theme_settings['settings']['color'] = array();
  1066. }
  1067. $theme_settings['settings']['color']['customGradient'] = ! $settings['disableCustomGradients'];
  1068. }
  1069. if ( isset( $settings['disableCustomFontSizes'] ) ) {
  1070. if ( ! isset( $theme_settings['settings']['typography'] ) ) {
  1071. $theme_settings['settings']['typography'] = array();
  1072. }
  1073. $theme_settings['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes'];
  1074. }
  1075. if ( isset( $settings['enableCustomLineHeight'] ) ) {
  1076. if ( ! isset( $theme_settings['settings']['typography'] ) ) {
  1077. $theme_settings['settings']['typography'] = array();
  1078. }
  1079. $theme_settings['settings']['typography']['customLineHeight'] = $settings['enableCustomLineHeight'];
  1080. }
  1081. if ( isset( $settings['enableCustomUnits'] ) ) {
  1082. if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
  1083. $theme_settings['settings']['spacing'] = array();
  1084. }
  1085. $theme_settings['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ?
  1086. array( 'px', 'em', 'rem', 'vh', 'vw', '%' ) :
  1087. $settings['enableCustomUnits'];
  1088. }
  1089. if ( isset( $settings['colors'] ) ) {
  1090. if ( ! isset( $theme_settings['settings']['color'] ) ) {
  1091. $theme_settings['settings']['color'] = array();
  1092. }
  1093. $theme_settings['settings']['color']['palette'] = $settings['colors'];
  1094. }
  1095. if ( isset( $settings['gradients'] ) ) {
  1096. if ( ! isset( $theme_settings['settings']['color'] ) ) {
  1097. $theme_settings['settings']['color'] = array();
  1098. }
  1099. $theme_settings['settings']['color']['gradients'] = $settings['gradients'];
  1100. }
  1101. if ( isset( $settings['fontSizes'] ) ) {
  1102. $font_sizes = $settings['fontSizes'];
  1103. // Back-compatibility for presets without units.
  1104. foreach ( $font_sizes as $key => $font_size ) {
  1105. if ( is_numeric( $font_size['size'] ) ) {
  1106. $font_sizes[ $key ]['size'] = $font_size['size'] . 'px';
  1107. }
  1108. }
  1109. if ( ! isset( $theme_settings['settings']['typography'] ) ) {
  1110. $theme_settings['settings']['typography'] = array();
  1111. }
  1112. $theme_settings['settings']['typography']['fontSizes'] = $font_sizes;
  1113. }
  1114. if ( isset( $settings['enableCustomSpacing'] ) ) {
  1115. if ( ! isset( $theme_settings['settings']['spacing'] ) ) {
  1116. $theme_settings['settings']['spacing'] = array();
  1117. }
  1118. $theme_settings['settings']['spacing']['customPadding'] = $settings['enableCustomSpacing'];
  1119. }
  1120. // Things that didn't land in core yet, so didn't have a setting assigned.
  1121. if ( current( (array) get_theme_support( 'experimental-link-color' ) ) ) {
  1122. if ( ! isset( $theme_settings['settings']['color'] ) ) {
  1123. $theme_settings['settings']['color'] = array();
  1124. }
  1125. $theme_settings['settings']['color']['link'] = true;
  1126. }
  1127. return $theme_settings;
  1128. }
  1129. }