CSS" transformer for the atomic style system, so a block's * local class and a reusable global class compile identically. * * A bucket is one set of style props for a given (breakpoint x interaction * state). Where the bucket lives is StyleBuckets' concern; which props exist * and what CSS they become is StylesSchema's. This class only turns one * bucket's props into declarations, and walks the tree to build rules. */ class AtomicStyles { /** * Compile a full bucket tree for one selector base into CSS. * * The stored state key IS the pseudo-selector, so it is appended directly; * the breakpoint comes from the bucket's device via the one media-query * builder. Buckets emit widest-first, which is what makes a narrower * breakpoint win in cascade mode. */ public static function compile_variants( $selector_base, $styles ) { return self::rules_to_css( self::compile_rules( $styles ), $selector_base ); } /** * Compile a bucket tree into a normalised rule list: * * [ [ media-query, state-selector, [ [ prop, value ], … ] ], … ] * * Values are cast to strings and the structure is a plain list, so the JSON * encoding is byte-identical to the JS mirror's `JSON.stringify` — that is * what makes the style hash reproducible across the editor and the front end. * * `$alignment` (the block's own alignment attribute, which lives outside the * bucket tree and is still device-suffixed) folds into each device's normal * state. It has to participate in the hash: two blocks with identical styles * but different alignment are not interchangeable. */ public static function compile_rules( $styles, $alignment = [] ) { $rules = []; $devices = Helper::get_responsive_devices(); $has_styles = StyleBuckets::has_schema_version( $styles ); foreach ( $devices as $device ) { $media = Helper::breakpoint_media_query( $device ); $bucket_key = StyleBuckets::device_bucket_key( $device ); foreach ( StyleBuckets::state_keys() as $state ) { $declarations = []; if ( $has_styles ) { $props = StyleBuckets::read_bucket( $styles, $bucket_key, $state ); if ( ! empty( $props ) ) { $declarations = self::apply_background_reset( self::state_declarations( $props ), '' === $state && '' === $bucket_key ); } } // Alignment applies to the normal state, last so it beats a // `textAlign` style prop set at the same level. if ( '' === $state && ! empty( $alignment ) ) { $declarations = array_merge( $declarations, Alignment::get_css( $alignment, 'text-align', $device['suffix'] ) ); } if ( empty( $declarations ) ) { continue; } $pairs = []; foreach ( $declarations as $property => $value ) { if ( '' === $value || null === $value ) { continue; } $pairs[] = [ (string) $property, (string) $value ]; } if ( ! empty( $pairs ) ) { $rules[] = [ $media, $state, $pairs ]; } } } return $rules; } /** * FNV-1a 32-bit over the normalised rule list. * * Deliberately not md5: the editor has to compute the identical hash at save * time, and FNV-1a is a handful of lines in both languages rather than a * crypto dependency in the editor bundle. */ public static function style_hash( $rules ) { $json = wp_json_encode( $rules, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); $hash = 2166136261; $len = strlen( $json ); for ( $i = 0; $i < $len; $i++ ) { $hash ^= ord( $json[ $i ] ); $hash = ( $hash * 16777619 ) & 0xFFFFFFFF; } return str_pad( dechex( $hash ), 8, '0', STR_PAD_LEFT ); } /** The shared style class for a bucket tree, or '' when it compiles to nothing. */ public static function style_class( $styles, $alignment = [] ) { $rules = self::compile_rules( $styles, $alignment ); return empty( $rules ) ? '' : 'ablocks-s-' . self::style_hash( $rules ); } /** Hashes already emitted this request, so each rule set is written once. */ private static $emitted = []; /** Forget what has been emitted (test/CLI helper). */ public static function reset_emitted() { self::$emitted = []; } /** * Register a block's compiled styles and return its shared class plus the * CSS that still needs emitting — empty on every block after the first with * the same rule set, which is where the duplicate-CSS reduction comes from. * * The class is repeated in the selector (0-2-0) so a block's own styles beat * an applied global class (0-1-0) without resorting to `!important`, which * would make global classes unable to override anything. */ public static function register_styles( $styles, $alignment = [] ) { $rules = self::compile_rules( $styles, $alignment ); if ( empty( $rules ) ) { return [ 'class' => '', 'css' => '' ]; } $hash = self::style_hash( $rules ); $class = 'ablocks-s-' . $hash; if ( isset( self::$emitted[ $hash ] ) ) { return [ 'class' => $class, 'css' => '' ]; } self::$emitted[ $hash ] = true; return [ 'class' => $class, 'css' => self::rules_to_css( $rules, '.' . $class . '.' . $class ) ]; } /** Render a normalised rule list against a selector base. */ public static function rules_to_css( $rules, $selector_base ) { $css = ''; foreach ( $rules as $rule ) { list( $media, $state, $pairs ) = $rule; $declarations = ''; foreach ( $pairs as $pair ) { // Escaped here rather than at the schema, so every kind of prop // — scalar, range, colour, typography, effect, overlay — passes // through one guard on its way out. This runs after style_hash() // has already read $rules, so the hash the editor writes into // the markup is unaffected. $declarations .= Helper::esc_css_value( $pair[0] ) . ':' . Helper::esc_css_value( $pair[1] ) . ';'; } $body = $selector_base . $state . '{' . $declarations . '}'; // A wrapping container's own children must size from their content, // or the line can never be over-subscribed and `flex-wrap` never // breaks one. That is a statement about THIS container's children, // so it is emitted as a child rule here rather than as an inherited // custom property: a custom property inherits down the whole tree, // so a wrapping container silently re-sized the children of every // non-wrapping container nested inside it — measured, a // non-wrapping inner container's children came out `flex-basis: // auto` (content-sized) instead of `0%` (equal share). // // Derived from the pairs rather than stored, so it costs nothing in // the bucket tree, and — because this runs after style_hash() has // read $rules — the hash in already-saved markup is unaffected. $body .= self::wrap_child_css( $pairs, $selector_base . $state ); $css .= ( '' !== $media ) ? $media . '{' . $body . '}' : $body; } return $css; } /** * The child rule a wrapping container needs, or ''. * * Scoped to container children only, matching the base stylesheets — a leaf * block is sized by its own block, not by the row it sits in. `:where()` * keeps the selector at the same specificity as those base rules, and this * so // authored CSS can't break out of the inline