/** * External dependencies */ import type { CssEditableProp, CssPropName, Maybe } from '@nab/types'; export function combinePropsForAst( props: ReadonlyArray< CssEditableProp > ): ReadonlyArray< CssEditableProp > { props = combineBackgroundProps( props ); props = combineBorderProps( props ); props = combineBorderRadiusProps( props ); return props; } // ======= // HELPERS // ======= function combineBackgroundProps( props: ReadonlyArray< CssEditableProp > ): ReadonlyArray< CssEditableProp > { const otherProps = props.filter( ( p ) => ! p.name.startsWith( 'background' ) ); const backgroundColor = getProp( 'backgroundColor', props ); const backgroundImage = getProp( 'backgroundImage', props ); if ( ! backgroundColor && ! backgroundImage ) { return otherProps; } const isImage = ( p: CssEditableProp ) => p.value.indexOf( 'url(' ) === 0; const isGradient = ( p: CssEditableProp ) => p.value.indexOf( 'gradient(' ) > 0; if ( backgroundImage && isImage( backgroundImage ) ) { const isCover = 'cover' === getProp( 'backgroundSize', props )?.value; const backgroundProps = props .filter( ( p ) => p.name.startsWith( 'background' ) && p !== backgroundColor && p !== backgroundImage ) .filter( ( p ) => p.name !== 'backgroundRepeat' || ! isCover ); return [ ...otherProps, { ...backgroundImage, name: 'background' }, ...backgroundProps, ]; } if ( backgroundImage && isGradient( backgroundImage ) ) { return [ ...otherProps, { ...backgroundImage, name: 'background' } ]; } return backgroundColor ? [ ...otherProps, { ...backgroundColor, name: 'background' } ] : otherProps; } function combineBorderProps( props: ReadonlyArray< CssEditableProp > ): ReadonlyArray< CssEditableProp > { const colors = { top: getProp( 'borderTopColor', props ), right: getProp( 'borderRightColor', props ), bottom: getProp( 'borderBottomColor', props ), left: getProp( 'borderLeftColor', props ), }; const styles = { top: getProp( 'borderTopStyle', props ), right: getProp( 'borderRightStyle', props ), bottom: getProp( 'borderBottomStyle', props ), left: getProp( 'borderLeftStyle', props ), }; const widths = { top: getProp( 'borderTopWidth', props ), right: getProp( 'borderRightWidth', props ), bottom: getProp( 'borderBottomWidth', props ), left: getProp( 'borderLeftWidth', props ), }; const borderProps = [ ...Object.values( colors ), ...Object.values( styles ), ...Object.values( widths ), ].filter( ( x ) => !! x ); const otherProps = exclude( borderProps, props ); if ( haveSameSideValues( colors ) && haveSameSideValues( styles ) && haveSameSideValues( widths ) ) { const color = colors.top; const style = styles.top; const width = widths.top; return [ ...otherProps, { ...color, name: 'border', value: `${ width.value } ${ style.value } ${ color.value }`, type: combinedType( [ width, style, color ] ), }, ]; } const unifySide = ( side: 'borderTop' | 'borderRight' | 'borderBottom' | 'borderLeft' ): Maybe< CssEditableProp > => { const key = side .toLowerCase() .replace( 'border', '' ) as keyof typeof colors; const color = colors[ key ]; const style = styles[ key ]; const width = widths[ key ]; if ( ! color?.value || ! style?.value || ! width?.value ) { return undefined; } return { ...color, name: side, value: `${ width.value } ${ style.value } ${ color.value }`, type: combinedType( [ width, style, color ] ), }; }; const top = unifySide( 'borderTop' ); const right = unifySide( 'borderRight' ); const bottom = unifySide( 'borderBottom' ); const left = unifySide( 'borderLeft' ); return [ ...props .filter( ( p ) => ! top || ! /border.*Top/.test( p.name ) ) .filter( ( p ) => ! right || ! /border.*Right/.test( p.name ) ) .filter( ( p ) => ! bottom || ! /border.*Bottom/.test( p.name ) ) .filter( ( p ) => ! left || ! /border.*Left/.test( p.name ) ), ...( top ? [ top ] : [] ), ...( bottom ? [ bottom ] : [] ), ...( left ? [ left ] : [] ), ...( right ? [ right ] : [] ), ]; } function combineBorderRadiusProps( props: ReadonlyArray< CssEditableProp > ): ReadonlyArray< CssEditableProp > { const border = { top: props.find( ( p ) => p.name === 'borderTopLeftRadius' ), right: props.find( ( p ) => p.name === 'borderTopRightRadius' ), bottom: props.find( ( p ) => p.name === 'borderBottomRightRadius' ), left: props.find( ( p ) => p.name === 'borderBottomLeftRadius' ), }; if ( ! canUnifySideValues( border ) ) { return props; } props = exclude( Object.values( border ), props ); return [ ...props, unifySideProps( 'borderRadius', border ) ]; } type SideValues = { readonly top: CssEditableProp; readonly right: CssEditableProp; readonly bottom: CssEditableProp; readonly left: CssEditableProp; }; const canUnifySideValues = ( sideValues: Partial< SideValues > ): sideValues is SideValues => !! sideValues.top?.value && !! sideValues.right?.value && !! sideValues.bottom?.value && !! sideValues.left?.value; const haveSameSideValues = ( sideValues: Partial< SideValues > ): sideValues is SideValues => canUnifySideValues( sideValues ) && sideValues.top.value === sideValues.right.value && sideValues.top.value === sideValues.bottom.value && sideValues.top.value === sideValues.left.value; function unifySideProps( newName: CssPropName, sideValues: SideValues ): CssEditableProp { return { ...sideValues.top, base: undefined, type: combinedType( sideValues ), name: newName, value: unifySideValues( sideValues ), }; } function unifySideValues( { top: { value: top }, right: { value: right }, bottom: { value: bottom }, left: { value: left }, }: SideValues ): string { if ( top === bottom ) { if ( right === left ) { if ( top === right ) { return top; } return `${ top } ${ right }`; } } if ( right === left ) { return `${ top } ${ right } ${ bottom }`; } return `${ top } ${ right } ${ bottom } ${ left }`; } const exclude = ( exclusions: ReadonlyArray< Maybe< CssEditableProp > >, props: ReadonlyArray< CssEditableProp > ): ReadonlyArray< CssEditableProp > => props.filter( ( p ) => ! exclusions.includes( p ) ); const important = ( props: | Record< string, CssEditableProp > | ReadonlyArray< Maybe< CssEditableProp > > ): boolean => Object.values( props ).some( ( p ) => 'important' === p?.type ); const combinedType = ( props: | Record< string, CssEditableProp > | ReadonlyArray< Maybe< CssEditableProp > > ): 'important' | 'regular' => ( important( props ) ? 'important' : 'regular' ); const getProp = ( name: CssPropName, props: ReadonlyArray< Maybe< CssEditableProp > > ): Maybe< CssEditableProp > => props.find( ( p ) => p?.name === name );