'@media only screen and (min-width: 1025px)', 'tablet' => '@media only screen and (min-width: 768px) and (max-width: 1024px)', 'mobile' => '@media only screen and (max-width: 767px)', ); /** * Border styles the block offers. Anything else is dropped rather than * being interpolated into a declaration. */ const BORDER_STYLES = array( 'none', 'solid', 'dotted', 'dashed', 'double', 'groove', 'ridge', 'inset', 'outset', ); /** * Layout attribute values, keyed by device. Used both to validate the * value before it reaches a selector and to know which alignment it is. */ const LAYOUTS = array( 'desktop' => array( 'desk_left_layout', 'desk_center_layout', 'desk_right_layout' ), 'tablet' => array( 'tab_left_layout', 'tab_center_layout', 'tab_right_layout' ), 'mobile' => array( 'mob_left_layout', 'mob_center_layout', 'mob_right_layout' ), ); /** * Default values, mirroring block.json. Kept here so that blocks saved * before an attribute existed still render the same as a fresh one. * * @return array */ public static function defaults() { return array( 'layout' => array( 'desktop' => 'desk_center_layout', 'tablet' => 'tab_center_layout', 'mobile' => 'mob_left_layout', ), 'verticalSpacing' => array( 'desktop' => 30, 'tablet' => 30, 'mobile' => 15, ), 'contentPadding' => array( 'desktop' => self::box( 20, true ), 'tablet' => self::box( 15, true ), 'mobile' => self::box( 10, true ), ), // Flat, not per-device: the frontend has only ever rendered the // desktop value at every breakpoint (see date_rule() below), so // tablet/mobile date padding controls have been removed rather // than kept as dead UI. 'datePadding' => self::box( 10, true ), 'contentBorder' => array( 'style' => 'solid', 'color' => '#406343', 'radius' => 0, 'width' => self::box( 1, true ), ), 'dateNameBorder' => array( 'style' => 'solid', 'color' => '#406343', 'radius' => 0, 'width' => self::box( 1, true ), ), 'markerBorder' => array( 'style' => 'solid', 'width' => 1, 'color' => '#406343', 'radius' => 50, ), 'markerSize' => array( 'desktop' => 15, 'tablet' => 15, 'mobile' => 15, ), 'markerContainerSize' => array( 'desktop' => 25, 'tablet' => 25, 'mobile' => 25, ), 'connector' => array( 'color' => '#406343', 'thickness' => 2, ), 'contentBg' => null, 'markerColor' => '#406343', // Null rather than #ffffff on purpose: when the attribute is absent // the stylesheet's own `background: #fff` should win, exactly as it // did before. Emitting a higher-specificity declaration here would // override themes that restyle the marker. 'markerBg' => null, 'dateNameColor' => null, 'dateNameBg' => null, 'zindex' => null, ); } /** * Build a four-sided box value. * * @param mixed $value Value for every side and for the linked value. * @param bool $linked Whether the sides start linked. * @return array */ private static function box( $value, $linked ) { return array( 'top' => $value, 'right' => $value, 'bottom' => $value, 'left' => $value, 'linked' => $linked, 'value' => $value, ); } /** * Accept either the pre-2.2 flat attributes or the grouped ones and return * the grouped shape. * * Both shapes have to be supported indefinitely: the `render_block` filter * receives attributes straight from the block comment, so a post that is * never re-opened in the editor keeps its flat keys forever. Gutenberg also * only serialises non-default attributes, so any key may simply be absent. * * @param array $attrs Raw block attributes. * @return array Grouped attributes, with every key present. */ public static function normalize( $attrs ) { $attrs = is_array( $attrs ) ? $attrs : array(); $out = self::defaults(); $has = static function ( $key ) use ( $attrs ) { return isset( $attrs[ $key ] ); }; $devices = array( 'desktop' => array( 'desk', 'D' ), 'tablet' => array( 'tab', 'T' ), 'mobile' => array( 'mob', 'M' ), ); // Layout is still stored flat (it is part of the saved markup). foreach ( $devices as $device => $prefixes ) { list( $prefix ) = $prefixes; $key = $prefix . 'Layout'; if ( $has( $key ) && in_array( $attrs[ $key ], self::LAYOUTS[ $device ], true ) ) { $out['layout'][ $device ] = $attrs[ $key ]; } } // Scalars are stored flat in both shapes. foreach ( array( 'contentBg', 'markerColor', 'markerBg', 'dateNameColor', 'dateNameBg', 'zindex' ) as $key ) { if ( $has( $key ) ) { $out[ $key ] = $attrs[ $key ]; } } // Legacy flat keys are folded in first so that the grouped shape, which // is what a migrated block stores, always wins over any leftovers. foreach ( $devices as $device => $prefixes ) { list( $prefix, $letter ) = $prefixes; if ( $has( $prefix . 'VerticalSpacing' ) ) { $out['verticalSpacing'][ $device ] = $attrs[ $prefix . 'VerticalSpacing' ]; } if ( $has( $prefix . 'MarkerSize' ) ) { $out['markerSize'][ $device ] = $attrs[ $prefix . 'MarkerSize' ]; } if ( $has( $prefix . 'MarkerContainerSize' ) ) { $out['markerContainerSize'][ $device ] = $attrs[ $prefix . 'MarkerContainerSize' ]; } $out['contentPadding'][ $device ] = self::legacy_box( $out['contentPadding'][ $device ], $attrs, 'content' . $letter . '%sPadding', 'enableContent' . $letter . 'linkedPadding', 'content' . $letter . 'linkedPadding', false ); } // Date padding only ever had a desktop (D) reading on the frontend, so // only the D-prefixed legacy keys are folded in; the T/M legacy values // were always dead and are intentionally dropped here. $out['datePadding'] = self::legacy_box( $out['datePadding'], $attrs, 'dateD%sPadding', 'enableDateDlinkedPadding', 'dateDlinkedPadding', false ); $out['contentBorder'] = self::legacy_border( $out['contentBorder'], $attrs, 'contentBorderStyle', 'contentBorderColor', 'contentBorderRadius', 'contentBorder%sWidth', 'enableLinkedBorder', 'contentBorderLinkedWidth' ); $out['dateNameBorder'] = self::legacy_border( $out['dateNameBorder'], $attrs, 'dateNameBorderStyle', 'dateNameBorderColor', 'dateNameBorderRadius', 'dateNameBorder%sWidth', 'enableDateNameLinkedBorder', 'dateNameBorderLinkedWidth' ); foreach ( array( 'markerBorderStyle' => 'style', 'markerBorderWidth' => 'width', 'markerBorderColor' => 'color', 'markerBorderRadius' => 'radius', ) as $legacy => $key ) { if ( $has( $legacy ) ) { $out['markerBorder'][ $key ] = $attrs[ $legacy ]; } } if ( $has( 'connectorColor' ) ) { $out['connector']['color'] = $attrs['connectorColor']; } if ( $has( 'connectorThickness' ) ) { $out['connector']['thickness'] = $attrs['connectorThickness']; } // Grouped shape last: it is the current format and takes precedence. foreach ( array( 'contentPadding', 'datePadding', 'contentBorder', 'dateNameBorder', 'markerBorder', 'markerSize', 'markerContainerSize', 'verticalSpacing', 'connector' ) as $key ) { if ( $has( $key ) && is_array( $attrs[ $key ] ) ) { $out[ $key ] = self::merge( $out[ $key ], $attrs[ $key ] ); } } return $out; } /** * Shallow-merge a grouped value over its defaults, one level deep. * * @param array $defaults Default values. * @param array $value Stored values. * @return array */ private static function merge( $defaults, $value ) { foreach ( $value as $key => $item ) { if ( isset( $defaults[ $key ] ) && is_array( $defaults[ $key ] ) && is_array( $item ) ) { $defaults[ $key ] = array_merge( $defaults[ $key ], $item ); } else { $defaults[ $key ] = $item; } } return $defaults; } /** * Fold the legacy per-side padding keys into a box value. * * @param array $box Current box value. * @param array $attrs Raw attributes. * @param string $side_pattern sprintf pattern for the per-side key. * @param string $linked_key Legacy linked-toggle key. * @param string $value_key Legacy linked-value key. * @param bool $ucfirst_side Whether the side is capitalised in the key. * Padding keys are contentDtopPadding (lower) * while border keys are contentBorderTopWidth * (upper), so this cannot be assumed. * @return array */ private static function legacy_box( $box, $attrs, $side_pattern, $linked_key, $value_key, $ucfirst_side = true ) { foreach ( array( 'top', 'right', 'bottom', 'left' ) as $side ) { $key = sprintf( $side_pattern, $ucfirst_side ? ucfirst( $side ) : $side ); if ( isset( $attrs[ $key ] ) ) { $box[ $side ] = $attrs[ $key ]; } } if ( isset( $attrs[ $linked_key ] ) ) { $box['linked'] = (bool) $attrs[ $linked_key ]; } if ( isset( $attrs[ $value_key ] ) ) { $box['value'] = $attrs[ $value_key ]; } return $box; } /** * Fold the legacy border keys into a grouped border value. * * @param array $border Current border value. * @param array $attrs Raw attributes. * @param string $style_key Legacy style key. * @param string $color_key Legacy colour key. * @param string $radius_key Legacy radius key. * @param string $side_pattern sprintf pattern for the per-side width key. * @param string $linked_key Legacy linked-toggle key. * @param string $value_key Legacy linked-width key. * @return array */ private static function legacy_border( $border, $attrs, $style_key, $color_key, $radius_key, $side_pattern, $linked_key, $value_key ) { if ( isset( $attrs[ $style_key ] ) ) { $border['style'] = $attrs[ $style_key ]; } if ( isset( $attrs[ $color_key ] ) ) { $border['color'] = $attrs[ $color_key ]; } if ( isset( $attrs[ $radius_key ] ) ) { $border['radius'] = $attrs[ $radius_key ]; } $border['width'] = self::legacy_box( $border['width'], $attrs, $side_pattern, $linked_key, $value_key ); return $border; } /** * Make a value safe to place inside a declaration. * * Colours may legitimately be hex, rgb()/hsl(), a named colour or a * `var(--wp--preset--color--*)` reference from a theme palette, so this * deliberately does not force hex. It only removes the characters that * could terminate the declaration, rule or