style; a length * the Size parser accepts, or a width keyword (thin/medium/thick) -> width; anything else -> color (the * catch-all, mirroring the raw-passthrough color converter). Only the parts present are emitted (omitted * parts are not reset to CSS initials). A second token for an already filled role is ambiguous, so the * whole expansion declines and the original shorthand is kept for custom_css. A produced longhand can * still individually decline downstream (e.g. `border-{side}-style`, which has no converter), degrading * to custom_css for that part only. */ class Border_Shorthand_Expander extends Shorthand_Expander_Base { const WIDTH_KEYWORDS = [ 'thin', 'medium', 'thick' ]; const ROLE_WIDTH = 'width'; const ROLE_STYLE = 'style'; const ROLE_COLOR = 'color'; private string $property; /** * @var array Role (width|style|color) -> emitted longhand property name. */ private array $longhands; /** * @var string[] */ private array $style_keywords; private ?Variables_Service $variables_service; /** * @param string $property The shorthand this expander owns (border, border-top, ...). * @param array $longhands Role -> longhand property name to emit. * @param string[] $style_keywords The border-style enum, sourced from the live schema. */ public function __construct( string $property, array $longhands, array $style_keywords, ?Variables_Service $variables_service = null ) { $this->property = $property; $this->longhands = $longhands; $this->style_keywords = $style_keywords; $this->variables_service = $variables_service; } protected function get_supported_properties(): array { return [ $this->property ]; } protected function expand_null( array $rule ): array { return array_map( fn( $p ) => $this->null_rule( $p ), array_values( $this->longhands ) ); } protected function do_expand( array $rule ): array { $value = $rule['value']; $tokens = Css_Token_Splitter::split_by_whitespace( trim( $value ) ); if ( empty( $tokens ) ) { return []; } $slots = [ self::ROLE_WIDTH => null, self::ROLE_STYLE => null, self::ROLE_COLOR => null, ]; foreach ( $tokens as $token ) { $role = $this->classify_token( $token ); if ( null === $role || null !== $slots[ $role ] ) { return []; } $slots[ $role ] = $token; } $rules = []; foreach ( $slots as $role => $slot_value ) { if ( null === $slot_value ) { continue; } $property = $this->longhands[ $role ]; $rules[] = [ 'property' => $property, 'value' => $slot_value, 'declaration' => $property . ': ' . $slot_value, ]; } return $rules; } private function classify_token( string $token ): ?string { if ( Css_Var_Token_Resolver::is_var_only_token( $token ) ) { $resolved_type = Css_Var_Token_Resolver::resolve_var_only_token_type( $this->variables_service, $token ); if ( 'color' === $resolved_type ) { return self::ROLE_COLOR; } if ( 'size' === $resolved_type ) { return self::ROLE_WIDTH; } return null; } return $this->classify_literal_token( $token ); } private function classify_literal_token( string $token ): string { $lower = strtolower( $token ); if ( in_array( $lower, $this->style_keywords, true ) ) { return self::ROLE_STYLE; } if ( in_array( $lower, self::WIDTH_KEYWORDS, true ) || null !== Size_Value_Parser::parse( $token ) ) { return self::ROLE_WIDTH; } return self::ROLE_COLOR; } }