diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index d8b8ad6771a07..400ebd0a84263 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -97,11 +97,8 @@ export function getInlineStyles( styles = {} ) { // The goal is to move everything to server side generated engine styles // This is temporary as we absorb more and more styles into the engine. - const extraRules = getCSSRules( styles, { selector: 'self' } ); + const extraRules = getCSSRules( styles ); extraRules.forEach( ( rule ) => { - if ( rule.selector !== 'self' ) { - throw "This style can't be added as inline style"; - } output[ rule.key ] = rule.value; } ); diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js index c583b53d96c0a..a5ed771d72d2f 100644 --- a/packages/blocks/src/api/constants.js +++ b/packages/blocks/src/api/constants.js @@ -28,6 +28,7 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { value: [ 'color', 'background' ], support: [ 'color', 'background' ], requiresOptOut: true, + useEngine: true, }, borderColor: { value: [ 'border', 'color' ], @@ -103,6 +104,7 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { value: [ 'color', 'text' ], support: [ 'color', 'text' ], requiresOptOut: true, + useEngine: true, }, filter: { value: [ 'filter', 'duotone' ], @@ -119,18 +121,22 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { fontSize: { value: [ 'typography', 'fontSize' ], support: [ 'typography', 'fontSize' ], + useEngine: true, }, fontStyle: { value: [ 'typography', 'fontStyle' ], support: [ 'typography', '__experimentalFontStyle' ], + useEngine: true, }, fontWeight: { value: [ 'typography', 'fontWeight' ], support: [ 'typography', '__experimentalFontWeight' ], + useEngine: true, }, lineHeight: { value: [ 'typography', 'lineHeight' ], support: [ 'typography', 'lineHeight' ], + useEngine: true, }, margin: { value: [ 'spacing', 'margin' ], @@ -157,14 +163,17 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { textDecoration: { value: [ 'typography', 'textDecoration' ], support: [ 'typography', '__experimentalTextDecoration' ], + useEngine: true, }, textTransform: { value: [ 'typography', 'textTransform' ], support: [ 'typography', '__experimentalTextTransform' ], + useEngine: true, }, letterSpacing: { value: [ 'typography', 'letterSpacing' ], support: [ 'typography', '__experimentalLetterSpacing' ], + useEngine: true, }, '--wp--style--block-gap': { value: [ 'spacing', 'blockGap' ], diff --git a/packages/edit-site/src/components/global-styles/use-global-styles-output.js b/packages/edit-site/src/components/global-styles/use-global-styles-output.js index e477faa83a024..0be47a7000b53 100644 --- a/packages/edit-site/src/components/global-styles/use-global-styles-output.js +++ b/packages/edit-site/src/components/global-styles/use-global-styles-output.js @@ -220,11 +220,8 @@ function getStylesDeclarations( blockStyles = {} ) { // The goal is to move everything to server side generated engine styles // This is temporary as we absorb more and more styles into the engine. - const extraRules = getCSSRules( blockStyles, { selector: 'self' } ); + const extraRules = getCSSRules( blockStyles ); extraRules.forEach( ( rule ) => { - if ( rule.selector !== 'self' ) { - throw "This style can't be added as inline style"; - } const cssProperty = rule.key.startsWith( '--' ) ? rule.key : kebabCase( rule.key ); diff --git a/packages/style-engine/src/index.ts b/packages/style-engine/src/index.ts index 8117e27892140..c78dd753834b6 100644 --- a/packages/style-engine/src/index.ts +++ b/packages/style-engine/src/index.ts @@ -24,6 +24,16 @@ import { styleDefinitions } from './styles'; */ export function generate( style: Style, options: StyleOptions ): string { const rules = getCSSRules( style, options ); + + // If no selector is provided, treat generated rules as inline styles to be returned as a single string. + if ( ! options?.selector ) { + const inlineRules: string[] = []; + rules.forEach( ( rule ) => { + inlineRules.push( `${ kebabCase( rule.key ) }: ${ rule.value };` ); + } ); + return inlineRules.join( ' ' ); + } + const groupedRules = groupBy( rules, 'selector' ); const selectorRules = Object.keys( groupedRules ).reduce( ( acc: string[], subSelector: string ) => { @@ -57,7 +67,9 @@ export function getCSSRules( ): GeneratedCSSRule[] { const rules: GeneratedCSSRule[] = []; styleDefinitions.forEach( ( definition: StyleDefinition ) => { - rules.push( ...definition.generate( style, options ) ); + if ( typeof definition.generate === 'function' ) { + rules.push( ...definition.generate( style, options ) ); + } } ); return rules; diff --git a/packages/style-engine/src/styles/color/background.ts b/packages/style-engine/src/styles/color/background.ts new file mode 100644 index 0000000000000..0df422bae7d58 --- /dev/null +++ b/packages/style-engine/src/styles/color/background.ts @@ -0,0 +1,19 @@ +/** + * Internal dependencies + */ +import type { Style, StyleOptions } from '../../types'; +import { generateRule } from '../utils'; + +const background = { + name: 'background', + generate: ( style: Style, options: StyleOptions ) => { + return generateRule( + style, + options, + [ 'color', 'background' ], + 'backgroundColor' + ); + }, +}; + +export default background; diff --git a/packages/style-engine/src/styles/color/gradient.ts b/packages/style-engine/src/styles/color/gradient.ts new file mode 100644 index 0000000000000..4cc0aecffbe63 --- /dev/null +++ b/packages/style-engine/src/styles/color/gradient.ts @@ -0,0 +1,19 @@ +/** + * Internal dependencies + */ +import type { Style, StyleOptions } from '../../types'; +import { generateRule } from '../utils'; + +const gradient = { + name: 'gradient', + generate: ( style: Style, options: StyleOptions ) => { + return generateRule( + style, + options, + [ 'color', 'gradient' ], + 'background' + ); + }, +}; + +export default gradient; diff --git a/packages/style-engine/src/styles/color/index.ts b/packages/style-engine/src/styles/color/index.ts new file mode 100644 index 0000000000000..15db28d726a80 --- /dev/null +++ b/packages/style-engine/src/styles/color/index.ts @@ -0,0 +1,8 @@ +/** + * Internal dependencies + */ +import background from './background'; +import gradient from './gradient'; +import text from './text'; + +export default [ text, gradient, background ]; diff --git a/packages/style-engine/src/styles/color/text.ts b/packages/style-engine/src/styles/color/text.ts new file mode 100644 index 0000000000000..e1a6bb3d99b5e --- /dev/null +++ b/packages/style-engine/src/styles/color/text.ts @@ -0,0 +1,14 @@ +/** + * Internal dependencies + */ +import type { Style, StyleOptions } from '../../types'; +import { generateRule } from '../utils'; + +const text = { + name: 'text', + generate: ( style: Style, options: StyleOptions ) => { + return generateRule( style, options, [ 'color', 'text' ], 'color' ); + }, +}; + +export default text; diff --git a/packages/style-engine/src/styles/constants.ts b/packages/style-engine/src/styles/constants.ts new file mode 100644 index 0000000000000..9df0cf255568e --- /dev/null +++ b/packages/style-engine/src/styles/constants.ts @@ -0,0 +1,3 @@ +export const VARIABLE_REFERENCE_PREFIX = 'var:'; +export const VARIABLE_PATH_SEPARATOR_TOKEN_ATTRIBUTE = '|'; +export const VARIABLE_PATH_SEPARATOR_TOKEN_STYLE = '--'; diff --git a/packages/style-engine/src/styles/index.ts b/packages/style-engine/src/styles/index.ts index f7c87a8c7a29b..79f1c43d8d33f 100644 --- a/packages/style-engine/src/styles/index.ts +++ b/packages/style-engine/src/styles/index.ts @@ -1,7 +1,8 @@ /** * Internal dependencies */ -import padding from './padding'; -import margin from './margin'; +import color from './color'; +import spacing from './spacing'; +import typography from './typography'; -export const styleDefinitions = [ margin, padding ]; +export const styleDefinitions = [ ...color, ...spacing, ...typography ]; diff --git a/packages/style-engine/src/styles/spacing/index.ts b/packages/style-engine/src/styles/spacing/index.ts new file mode 100644 index 0000000000000..4f6121951b0bf --- /dev/null +++ b/packages/style-engine/src/styles/spacing/index.ts @@ -0,0 +1,7 @@ +/** + * Internal dependencies + */ +import padding from './padding'; +import margin from './margin'; + +export default [ margin, padding ]; diff --git a/packages/style-engine/src/styles/margin.ts b/packages/style-engine/src/styles/spacing/margin.ts similarity index 71% rename from packages/style-engine/src/styles/margin.ts rename to packages/style-engine/src/styles/spacing/margin.ts index 2ead5da23f1e7..c8492c378954f 100644 --- a/packages/style-engine/src/styles/margin.ts +++ b/packages/style-engine/src/styles/spacing/margin.ts @@ -1,8 +1,8 @@ /** * Internal dependencies */ -import type { Style, StyleOptions } from '../types'; -import { generateBoxRules } from './utils'; +import type { Style, StyleOptions } from '../../types'; +import { generateBoxRules } from '../utils'; const margin = { name: 'margin', diff --git a/packages/style-engine/src/styles/padding.ts b/packages/style-engine/src/styles/spacing/padding.ts similarity index 71% rename from packages/style-engine/src/styles/padding.ts rename to packages/style-engine/src/styles/spacing/padding.ts index 94b268de35604..a5a3227030e07 100644 --- a/packages/style-engine/src/styles/padding.ts +++ b/packages/style-engine/src/styles/spacing/padding.ts @@ -1,8 +1,8 @@ /** * Internal dependencies */ -import type { Style, StyleOptions } from '../types'; -import { generateBoxRules } from './utils'; +import type { Style, StyleOptions } from '../../types'; +import { generateBoxRules } from '../utils'; const padding = { name: 'padding', diff --git a/packages/style-engine/src/styles/typography/index.ts b/packages/style-engine/src/styles/typography/index.ts new file mode 100644 index 0000000000000..70e32b23cafe7 --- /dev/null +++ b/packages/style-engine/src/styles/typography/index.ts @@ -0,0 +1,99 @@ +/** + * Internal dependencies + */ +import type { Style, StyleOptions } from '../../types'; +import { generateRule } from '../utils'; + +const fontSize = { + name: 'fontSize', + generate: ( style: Style, options: StyleOptions ) => { + return generateRule( + style, + options, + [ 'typography', 'fontSize' ], + 'fontSize' + ); + }, +}; + +const fontStyle = { + name: 'fontStyle', + generate: ( style: Style, options: StyleOptions ) => { + return generateRule( + style, + options, + [ 'typography', 'fontStyle' ], + 'fontStyle' + ); + }, +}; + +const fontWeight = { + name: 'fontWeight', + generate: ( style: Style, options: StyleOptions ) => { + return generateRule( + style, + options, + [ 'typography', 'fontWeight' ], + 'fontWeight' + ); + }, +}; + +const letterSpacing = { + name: 'letterSpacing', + generate: ( style: Style, options: StyleOptions ) => { + return generateRule( + style, + options, + [ 'typography', 'letterSpacing' ], + 'letterSpacing' + ); + }, +}; + +const lineHeight = { + name: 'letterSpacing', + generate: ( style: Style, options: StyleOptions ) => { + return generateRule( + style, + options, + [ 'typography', 'lineHeight' ], + 'lineHeight' + ); + }, +}; + +const textDecoration = { + name: 'textDecoration', + generate: ( style: Style, options: StyleOptions ) => { + return generateRule( + style, + options, + [ 'typography', 'textDecoration' ], + 'textDecoration' + ); + }, +}; + +const textTransform = { + name: 'textTransform', + generate: ( style: Style, options: StyleOptions ) => { + return generateRule( + style, + options, + [ 'typography', 'textTransform' ], + 'textTransform' + ); + }, +}; + +export default [ + fontSize, + fontStyle, + fontWeight, + letterSpacing, + lineHeight, + textDecoration, + textTransform, +]; diff --git a/packages/style-engine/src/styles/utils.ts b/packages/style-engine/src/styles/utils.ts index 5455901e49d1a..928d923c86821 100644 --- a/packages/style-engine/src/styles/utils.ts +++ b/packages/style-engine/src/styles/utils.ts @@ -7,7 +7,51 @@ import { get, upperFirst } from 'lodash'; * Internal dependencies */ import type { GeneratedCSSRule, Style, Box, StyleOptions } from '../types'; +import { + VARIABLE_REFERENCE_PREFIX, + VARIABLE_PATH_SEPARATOR_TOKEN_ATTRIBUTE, + VARIABLE_PATH_SEPARATOR_TOKEN_STYLE, +} from './constants'; +/** + * Returns a JSON representation of the generated CSS rules. + * + * @param style Style object. + * @param options Options object with settings to adjust how the styles are generated. + * @param path An array of strings representing the path to the style value in the style object. + * @param ruleKey A CSS property key. + * + * @return GeneratedCSSRule[] CSS rules. + */ +export function generateRule( + style: Style, + options: StyleOptions, + path: string[], + ruleKey: string +) { + const styleValue: string | undefined = get( style, path ); + + return styleValue + ? [ + { + selector: options?.selector, + key: ruleKey, + value: getCSSVarFromStyleValue( styleValue ), + }, + ] + : []; +} + +/** + * Returns a JSON representation of the generated CSS rules taking into account box model properties, top, right, bottom, left. + * + * @param style Style object. + * @param options Options object with settings to adjust how the styles are generated. + * @param path An array of strings representing the path to the style value in the style object. + * @param ruleKey A CSS property key. + * + * @return GeneratedCSSRule[] CSS rules. + */ export function generateBoxRules( style: Style, options: StyleOptions, @@ -22,7 +66,7 @@ export function generateBoxRules( const rules: GeneratedCSSRule[] = []; if ( typeof boxStyle === 'string' ) { rules.push( { - selector: options.selector, + selector: options?.selector, key: ruleKey, value: boxStyle, } ); @@ -32,7 +76,7 @@ export function generateBoxRules( const value: string | undefined = get( boxStyle, [ side ] ); if ( value ) { acc.push( { - selector: options.selector, + selector: options?.selector, key: `${ ruleKey }${ upperFirst( side ) }`, value, } ); @@ -46,3 +90,24 @@ export function generateBoxRules( return rules; } + +/** + * Returns a CSS var value from incoming style value following the pattern `var:description|context|slug`. + * + * @param styleValue A raw style value. + * + * @return string A CSS var value. + */ +export function getCSSVarFromStyleValue( styleValue: string ): string { + if ( + typeof styleValue === 'string' && + styleValue.startsWith( VARIABLE_REFERENCE_PREFIX ) + ) { + const variable = styleValue + .slice( VARIABLE_REFERENCE_PREFIX.length ) + .split( VARIABLE_PATH_SEPARATOR_TOKEN_ATTRIBUTE ) + .join( VARIABLE_PATH_SEPARATOR_TOKEN_STYLE ); + return `var(--wp--${ variable })`; + } + return styleValue; +} diff --git a/packages/style-engine/src/test/index.js b/packages/style-engine/src/test/index.js index bf1ab73566f52..ad2acd401056b 100644 --- a/packages/style-engine/src/test/index.js +++ b/packages/style-engine/src/test/index.js @@ -8,7 +8,23 @@ describe( 'generate', () => { expect( generate( {}, '.some-selector' ) ).toEqual( '' ); } ); - it( 'should generate spacing styles', () => { + it( 'should generate inline styles where there is no selector', () => { + expect( + generate( { + spacing: { padding: '10px', margin: '12px' }, + color: { + text: '#f1f1f1', + background: '#222222', + gradient: + 'linear-gradient(135deg,rgb(6,147,227) 0%,rgb(143,47,47) 49%,rgb(155,81,224) 100%)', + }, + } ) + ).toEqual( + 'color: #f1f1f1; background: linear-gradient(135deg,rgb(6,147,227) 0%,rgb(143,47,47) 49%,rgb(155,81,224) 100%); background-color: #222222; margin: 12px; padding: 10px;' + ); + } ); + + it( 'should generate styles with an optional selector', () => { expect( generate( { @@ -23,6 +39,10 @@ describe( 'generate', () => { expect( generate( { + color: { + text: '#cccccc', + background: '#111111', + }, spacing: { padding: { top: '10px', bottom: '5px' }, margin: { @@ -32,15 +52,35 @@ describe( 'generate', () => { left: '14px', }, }, + typography: { + fontSize: '2.2rem', + fontStyle: 'italic', + fontWeight: '800', + fontFamily: "'Helvetica Neue',sans-serif", + lineHeight: '3.3', + textDecoration: 'line-through', + letterSpacing: '12px', + textTransform: 'uppercase', + }, }, { selector: '.some-selector', } ) ).toEqual( - '.some-selector { margin-top: 11px; margin-right: 12px; margin-bottom: 13px; margin-left: 14px; padding-top: 10px; padding-bottom: 5px; }' + '.some-selector { color: #cccccc; background-color: #111111; margin-top: 11px; margin-right: 12px; margin-bottom: 13px; margin-left: 14px; padding-top: 10px; padding-bottom: 5px; font-size: 2.2rem; font-style: italic; font-weight: 800; letter-spacing: 12px; line-height: 3.3; text-decoration: line-through; text-transform: uppercase; }' ); } ); + + it( 'should parse preset values (use for elements.link.color.text)', () => { + expect( + generate( { + color: { + text: 'var:preset|color|ham-sandwich', + }, + } ) + ).toEqual( 'color: var(--wp--preset--color--ham-sandwich);' ); + } ); } ); describe( 'getCSSRules', () => { @@ -96,16 +136,40 @@ describe( 'getCSSRules', () => { expect( getCSSRules( { + color: { + text: '#dddddd', + background: '#555555', + }, spacing: { padding: { top: '10px', bottom: '5px' }, margin: { right: '2em', left: '1vw' }, }, + typography: { + fontSize: '2.2rem', + fontStyle: 'italic', + fontWeight: '800', + fontFamily: "'Helvetica Neue',sans-serif", + lineHeight: '3.3', + textDecoration: 'line-through', + letterSpacing: '12px', + textTransform: 'uppercase', + }, }, { selector: '.some-selector', } ) ).toEqual( [ + { + selector: '.some-selector', + key: 'color', + value: '#dddddd', + }, + { + selector: '.some-selector', + key: 'backgroundColor', + value: '#555555', + }, { selector: '.some-selector', key: 'marginRight', @@ -126,6 +190,41 @@ describe( 'getCSSRules', () => { key: 'paddingBottom', value: '5px', }, + { + key: 'fontSize', + selector: '.some-selector', + value: '2.2rem', + }, + { + key: 'fontStyle', + selector: '.some-selector', + value: 'italic', + }, + { + key: 'fontWeight', + selector: '.some-selector', + value: '800', + }, + { + key: 'letterSpacing', + selector: '.some-selector', + value: '12px', + }, + { + key: 'lineHeight', + selector: '.some-selector', + value: '3.3', + }, + { + key: 'textDecoration', + selector: '.some-selector', + value: 'line-through', + }, + { + key: 'textTransform', + selector: '.some-selector', + value: 'uppercase', + }, ] ); } ); } ); diff --git a/packages/style-engine/src/types.ts b/packages/style-engine/src/types.ts index 48bd9a0d8e5c6..9f60b1b0f6fc0 100644 --- a/packages/style-engine/src/types.ts +++ b/packages/style-engine/src/types.ts @@ -26,6 +26,18 @@ export interface Style { textDecoration?: CSSProperties[ 'textDecoration' ]; textTransform?: CSSProperties[ 'textTransform' ]; }; + color?: { + text?: CSSProperties[ 'color' ]; + background?: CSSProperties[ 'backgroundColor' ]; + gradient?: CSSProperties[ 'background' ]; + }; + elements?: { + link?: { + color?: { + text?: CSSProperties[ 'color' ]; + }; + }; + }; } export type StyleOptions = { @@ -47,5 +59,6 @@ export type GeneratedCSSRule = { export interface StyleDefinition { name: string; - generate: ( style: Style, options: StyleOptions ) => GeneratedCSSRule[]; + generate?: ( style: Style, options: StyleOptions ) => GeneratedCSSRule[]; + getClassNames?: ( style: Style ) => string[]; } diff --git a/test/integration/fixtures/blocks/core__button__squared.serialized.html b/test/integration/fixtures/blocks/core__button__squared.serialized.html index 29e7fed15a510..ec83bb66b4971 100644 --- a/test/integration/fixtures/blocks/core__button__squared.serialized.html +++ b/test/integration/fixtures/blocks/core__button__squared.serialized.html @@ -1,3 +1,3 @@ -
+ diff --git a/test/integration/fixtures/blocks/core__pullquote__custom-colors.serialized.html b/test/integration/fixtures/blocks/core__pullquote__custom-colors.serialized.html index 3427b0ff08798..d6b9f285fdecb 100644 --- a/test/integration/fixtures/blocks/core__pullquote__custom-colors.serialized.html +++ b/test/integration/fixtures/blocks/core__pullquote__custom-colors.serialized.html @@ -1,3 +1,3 @@ -pullquote
citation
pullquote
citation