diff --git a/docs/manifest.json b/docs/manifest.json index 027ae8e86e1429..52fbe608db945f 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1757,6 +1757,12 @@ "markdown_source": "../packages/shortcode/README.md", "parent": "packages" }, + { + "title": "@wordpress/style-engine", + "slug": "packages-style-engine", + "markdown_source": "../packages/style-engine/README.md", + "parent": "packages" + }, { "title": "@wordpress/stylelint-config", "slug": "packages-stylelint-config", diff --git a/package-lock.json b/package-lock.json index 39b1b228d5aba0..c0f224198751f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15784,6 +15784,7 @@ "@wordpress/notices": "file:packages/notices", "@wordpress/rich-text": "file:packages/rich-text", "@wordpress/shortcode": "file:packages/shortcode", + "@wordpress/style-engine": "file:packages/style-engine", "@wordpress/token-list": "file:packages/token-list", "@wordpress/url": "file:packages/url", "@wordpress/warning": "file:packages/warning", @@ -16269,6 +16270,7 @@ "@wordpress/plugins": "file:packages/plugins", "@wordpress/primitives": "file:packages/primitives", "@wordpress/reusable-blocks": "file:packages/reusable-blocks", + "@wordpress/style-engine": "file:packages/style-engine", "@wordpress/url": "file:packages/url", "@wordpress/viewport": "file:packages/viewport", "classnames": "^2.3.1", @@ -16982,6 +16984,13 @@ "memize": "^1.1.0" } }, + "@wordpress/style-engine": { + "version": "file:packages/style-engine", + "requires": { + "@babel/runtime": "^7.16.0", + "lodash": "^4.17.21" + } + }, "@wordpress/stylelint-config": { "version": "file:packages/stylelint-config", "dev": true, @@ -19313,16 +19322,6 @@ "yauzl": "^2.7.0" }, "dependencies": { - "are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "dev": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - } - }, "gauge": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.1.tgz", diff --git a/package.json b/package.json index 336abb58389e59..3ae95708706044 100755 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "@wordpress/rich-text": "file:packages/rich-text", "@wordpress/server-side-render": "file:packages/server-side-render", "@wordpress/shortcode": "file:packages/shortcode", + "@wordpress/style-engine": "file:packages/style-engine", "@wordpress/token-list": "file:packages/token-list", "@wordpress/url": "file:packages/url", "@wordpress/viewport": "file:packages/viewport", diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index 1609b7eca695e9..4ceaab8c2fdf56 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -54,6 +54,7 @@ "@wordpress/notices": "file:../notices", "@wordpress/rich-text": "file:../rich-text", "@wordpress/shortcode": "file:../shortcode", + "@wordpress/style-engine": "file:../style-engine", "@wordpress/token-list": "file:../token-list", "@wordpress/url": "file:../url", "@wordpress/warning": "file:../warning", diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index 5882d5275937be..d15851f05b22cd 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -27,6 +27,7 @@ import { __EXPERIMENTAL_ELEMENTS as ELEMENTS, } from '@wordpress/blocks'; import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; +import { getCSSRules } from '@wordpress/style-engine'; /** * Internal dependencies @@ -85,21 +86,35 @@ export function getInlineStyles( styles = {} ) { // option and backwards compatibility for border radius support. const styleValue = get( styles, path ); - if ( !! subPaths && ! isString( styleValue ) ) { - Object.entries( subPaths ).forEach( ( entry ) => { - const [ name, subPath ] = entry; - const value = get( styleValue, [ subPath ] ); - - if ( value ) { - output[ name ] = compileStyleValue( value ); - } - } ); - } else if ( ! ignoredStyles.includes( path.join( '.' ) ) ) { - output[ propKey ] = compileStyleValue( get( styles, path ) ); + if ( ! styleValue.useEngine ) { + if ( !! subPaths && ! isString( styleValue ) ) { + Object.entries( subPaths ).forEach( ( entry ) => { + const [ name, subPath ] = entry; + const value = get( styleValue, [ subPath ] ); + + if ( value ) { + output[ name ] = compileStyleValue( value ); + } + } ); + } else if ( ! ignoredStyles.includes( path.join( '.' ) ) ) { + output[ propKey ] = compileStyleValue( + get( styles, path ) + ); + } } } } ); + // 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, 'self' ); + extraRules.forEach( ( rule ) => { + if ( rule.selector !== 'self' ) { + throw "This style can't be added as inline style"; + } + return ( output[ rule.key ] = rule.value ); + } ); + return output; } diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js index 674caea6af4c99..a237402e143f9d 100644 --- a/packages/blocks/src/api/constants.js +++ b/packages/blocks/src/api/constants.js @@ -99,6 +99,7 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { paddingBottom: 'bottom', paddingLeft: 'left', }, + useEngine: true, }, textDecoration: { value: [ 'typography', 'textDecoration' ], diff --git a/packages/dependency-extraction-webpack-plugin/lib/util.js b/packages/dependency-extraction-webpack-plugin/lib/util.js index a22837af6a72e9..87d67f8dbd68fd 100644 --- a/packages/dependency-extraction-webpack-plugin/lib/util.js +++ b/packages/dependency-extraction-webpack-plugin/lib/util.js @@ -1,5 +1,9 @@ const WORDPRESS_NAMESPACE = '@wordpress/'; -const BUNDLED_PACKAGES = [ '@wordpress/icons', '@wordpress/interface' ]; +const BUNDLED_PACKAGES = [ + '@wordpress/icons', + '@wordpress/interface', + '@wordpress/style-engine', +]; /** * Default request to global transformation diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index 325f86ff5a1011..0c8e2d00a20fb2 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -51,6 +51,7 @@ "@wordpress/plugins": "file:../plugins", "@wordpress/primitives": "file:../primitives", "@wordpress/reusable-blocks": "file:../reusable-blocks", + "@wordpress/style-engine": "file:../style-engine", "@wordpress/url": "file:../url", "@wordpress/viewport": "file:../viewport", "classnames": "^2.3.1", 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 b3ca7bd9ba0167..a5af190334212b 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 @@ -23,6 +23,7 @@ import { getBlockTypes, } from '@wordpress/blocks'; import { useEffect, useState, useContext } from '@wordpress/element'; +import { getCSSRules } from '@wordpress/style-engine'; /** * Internal dependencies @@ -146,7 +147,7 @@ function flattenTree( input = {}, prefix, token ) { * @return {Array} An array of style declarations. */ function getStylesDeclarations( blockStyles = {} ) { - return reduce( + const output = reduce( STYLE_PROPERTY, ( declarations, { value, properties }, key ) => { const pathToValue = value; @@ -188,6 +189,18 @@ 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, 'self' ); + extraRules.forEach( ( rule ) => { + if ( rule.selector !== 'self' ) { + throw "This style can't be added as inline style"; + } + return ( output[ rule.key ] = compileStyleValue( rule.value ) ); + } ); + + return output; } export const getNodesWithStyles = ( tree, blockSelectors ) => { diff --git a/packages/style-engine/.npmrc b/packages/style-engine/.npmrc new file mode 100644 index 00000000000000..43c97e719a5a82 --- /dev/null +++ b/packages/style-engine/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/style-engine/CHANGELOG.md b/packages/style-engine/CHANGELOG.md new file mode 100644 index 00000000000000..330d83fcc88269 --- /dev/null +++ b/packages/style-engine/CHANGELOG.md @@ -0,0 +1,121 @@ + + +## Unreleased + +## 3.3.0 (2021-11-07) + +### New Feature + +- Added new `normalizePath` function ([#35992](https://github.com/WordPress/gutenberg/pull/35992)). + +## 3.2.3 (2021-10-12) + +### Bug Fix + +- Removed unused `react-native-url-polyfill` dependency ([#34687](https://github.com/WordPress/gutenberg/pull/34687)). + +## 3.2.0 (2021-07-21) + +## 3.1.0 (2021-05-20) + +## 3.0.0 (2021-05-14) + +### Breaking Changes + +- Drop support for Internet Explorer 11 ([#31110](https://github.com/WordPress/gutenberg/pull/31110)). Learn more at https://make.wordpress.org/core/2021/04/22/ie-11-support-phase-out-plan/. +- Increase the minimum Node.js version to v12 matching Long Term Support releases ([#31270](https://github.com/WordPress/gutenberg/pull/31270)). Learn more at https://nodejs.org/en/about/releases/. + +## 2.22.0 (2021-03-17) + +## 2.21.0 (2021-01-05) + +### New Feature + +- Add optional argument `maxLength` for truncating URL in `filterURLForDisplay` + +## 2.16.0 (2020-06-15) + +### New Feature + +- Added `getPathAndQueryString`. + +## 2.14.0 (2020-04-30) + +### Bug fix + +- `addQueryArgs` arguments are optional ([#21926](https://github.com/WordPress/gutenberg/pull/21926)) + +## 2.13.0 (2020-04-15) + +### New feature + +- Include TypeScript type declarations ([#18942](https://github.com/WordPress/gutenberg/pull/18942)) + +# 2.12.0 (2020-04-01) + +### Bug Fixes + +- `getQueryString` now correctly considers hash fragments when considering whether to return a query string. Previously, `getQueryString( 'https://example.com/#?foo' )` would wrongly return `'foo'` as its result. A hash fragment is always the last segment of a URL, and the querystring must always precede it ([see reference specification](https://url.spec.whatwg.org/#absolute-url-with-fragment-string)). + +## 2.11.0 (2020-02-10) + +### Bug Fixes + +- `isURL` now correctly returns `true` for many other forms of a valid URL, as it now conforms to the [URL Living Standard](https://url.spec.whatwg.org/) definition of a [valid URL string](https://url.spec.whatwg.org/#valid-url-string). + +## 2.3.3 (2019-01-03) + +### Bug Fixes + +- `addQueryArgs` will return only the querystring fragment if the passed `url` is undefined. Previously, an uncaught error would be thrown. +- `addQueryArgs` will not append (or remove) a `?` if there are no query arguments to be added. Previously, `?` would be wrongly appended even if there was no querystring generated. + +## 2.3.2 (2018-12-12) + +## 2.3.1 (2018-11-20) + +### Bug fixes + +- The `isValidProtocol` function now correctly considers the protocol of the URL as only incoporating characters up to and including the colon (':'). +- `getFragment` is now greedier and matches fragments from the first occurence of the '#' symbol instead of the last. + +## 2.3.0 (2018-11-12) + +### New Features + +- Added `getProtocol`. +- Added `isValidProtocol`. +- Added `getAuthority` +- Added `isValidAuthority`. +- Added `getPath`. +- Added `isValidPath`. +- Added `getQueryString`. +- Added `isValidQueryString`. +- Added `getFragment`. +- Added `isValidFragment`. + +## 2.2.0 (2018-10-29) + +### New Features + +- Added `getQueryArg`. +- Added `hasQueryArg`. +- Added `removeQueryArgs`. + +## 2.1.0 (2018-10-16) + +### New Feature + +- Added `safeDecodeURI`. + +## 2.0.1 (2018-09-30) + +### Bug Fix + +- Fix typo in the `qs` dependency definition in the `package.json` + +## 2.0.0 (2018-09-05) + +### Breaking Change + +- Change how required built-ins are polyfilled with Babel 7 ([#9171](https://github.com/WordPress/gutenberg/pull/9171)). If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. diff --git a/packages/style-engine/README.md b/packages/style-engine/README.md new file mode 100644 index 00000000000000..1d410dd2553799 --- /dev/null +++ b/packages/style-engine/README.md @@ -0,0 +1,62 @@ +# Style Engine + +The Style Engine powering global styles and block customizations. + +## Installation + +Install the module + +```bash +npm install @wordpress/style-engine --save +``` + +_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for such language features and APIs, you should include [the polyfill shipped in `@wordpress/babel-preset-default`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/babel-preset-default#polyfill) in your code._ + +## Important + +This Package is considered experimental at the moment. The idea is to have a package used to generate styles based on a style object that is consistent between: backend, frontend, block style object and theme.json. + +Currently it's not a package that generates a wp.styleEngine global because it's not ready yet, it's still a bundled package but ultimately, we want it to be so, once the roadmap is finished: + +**TODO List:** + +- Add style definitions for all the currently supported styles in blocks and theme.json. +- the CSS variable shortcuts for values (for presets...) +- Support generating styles in the frontend. +- Support generating styles in the backend (block supports and theme.json stylesheet). +- Refactor all block styles to use the style engine server side. +- Refactor all blocks to consistently use the "style" attribute for all customizations (get rid of the preset specific attributes). + +## Usage + + + +### generate + +Generates a stylesheet for a given style object and selector. + +_Parameters_ + +- _style_ `Style`: Style object. +- _selector_ `string`: CSS selector. + +_Returns_ + +- `string`: generated stylesheet. + +### getCSSRules + +Returns a JSON representation of the generated CSS rules. + +_Parameters_ + +- _style_ `Style`: Style object. +- _selector_ `string`: CSS selector. + +_Returns_ + +- `GeneratedCSSRule[]`: generated styles. + + + +

Code is Poetry.

diff --git a/packages/style-engine/package.json b/packages/style-engine/package.json new file mode 100644 index 00000000000000..74512443ba912d --- /dev/null +++ b/packages/style-engine/package.json @@ -0,0 +1,37 @@ +{ + "name": "@wordpress/style-engine", + "version": "0.1.0", + "description": "WordPress Style engine.", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress", + "gutenberg", + "styles", + "global styles" + ], + "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/style-engine/README.md", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/gutenberg.git", + "directory": "packages/style-engine" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "engines": { + "node": ">=12" + }, + "main": "build/index.js", + "module": "build-module/index.js", + "react-native": "src/index", + "types": "build-types", + "sideEffects": false, + "dependencies": { + "@babel/runtime": "^7.16.0", + "lodash": "^4.17.21" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/style-engine/src/index.ts b/packages/style-engine/src/index.ts new file mode 100644 index 00000000000000..2fad43edb70344 --- /dev/null +++ b/packages/style-engine/src/index.ts @@ -0,0 +1,59 @@ +/** + * External dependencies + */ +import { groupBy } from 'lodash'; + +/** + * Internal dependencies + */ +import type { Style, GeneratedCSSRule, StyleDefinition } from './types'; +import { styleDefinitions } from './styles'; + +/** + * Generates a stylesheet for a given style object and selector. + * + * @param style Style object. + * @param selector CSS selector. + * + * @return generated stylesheet. + */ +export function generate( style: Style, selector: string ): string { + const rules = getCSSRules( style, selector ); + const groupedRules = groupBy( rules, 'selector' ); + const selectorRules = Object.keys( groupedRules ).reduce( + ( acc: string[], subSelector: string ) => { + acc.push( + `${ subSelector } { ${ groupedRules[ subSelector ] + .map( + ( rule: GeneratedCSSRule ) => + `${ rule.key }: ${ rule.value };` + ) + .join( ' ' ) } }` + ); + return acc; + }, + [] + ); + + return selectorRules.join( '\n' ); +} + +/** + * Returns a JSON representation of the generated CSS rules. + * + * @param style Style object. + * @param selector CSS selector. + * + * @return generated styles. + */ +export function getCSSRules( + style: Style, + selector: string +): GeneratedCSSRule[] { + let rules: GeneratedCSSRule[] = []; + styleDefinitions.forEach( ( definition: StyleDefinition ) => { + rules = [ ...rules, ...definition.generate( style, selector ) ]; + } ); + + return rules; +} diff --git a/packages/style-engine/src/styles/index.ts b/packages/style-engine/src/styles/index.ts new file mode 100644 index 00000000000000..2f09e428176937 --- /dev/null +++ b/packages/style-engine/src/styles/index.ts @@ -0,0 +1,6 @@ +/** + * Internal dependencies + */ +import padding from './padding'; + +export const styleDefinitions = [ padding ]; diff --git a/packages/style-engine/src/styles/padding.ts b/packages/style-engine/src/styles/padding.ts new file mode 100644 index 00000000000000..8c8485edb479e9 --- /dev/null +++ b/packages/style-engine/src/styles/padding.ts @@ -0,0 +1,19 @@ +/** + * Internal dependencies + */ +import type { Style } from '../types'; +import { generateBoxRules } from './utils'; + +const padding = { + name: 'padding', + generate: ( style: Style, selector: string ) => { + return generateBoxRules( + style, + selector, + [ 'spacing', 'padding' ], + 'padding' + ); + }, +}; + +export default padding; diff --git a/packages/style-engine/src/styles/utils.ts b/packages/style-engine/src/styles/utils.ts new file mode 100644 index 00000000000000..76efe97391a25e --- /dev/null +++ b/packages/style-engine/src/styles/utils.ts @@ -0,0 +1,44 @@ +/** + * External dependencies + */ +import { get } from 'lodash'; + +/** + * Internal dependencies + */ +import type { GeneratedCSSRule, Style, Box } from '../types'; + +export function generateBoxRules( + style: Style, + selector: string, + path: string[], + ruleKey: string +): GeneratedCSSRule[] { + const boxStyle: Box | string | undefined = get( style, path ); + if ( ! boxStyle ) { + return []; + } + + const rules: GeneratedCSSRule[] = []; + if ( typeof boxStyle === 'string' ) { + rules.push( { selector, key: ruleKey, value: boxStyle } ); + } else { + const sideRules = [ 'top', 'bottom', 'left', 'right' ].reduce( + ( acc: GeneratedCSSRule[], side: string ) => { + const value: string | undefined = get( boxStyle, [ side ] ); + if ( value ) { + acc.push( { + selector, + key: `${ ruleKey }-${ side }`, + value, + } ); + } + return acc; + }, + [] + ); + rules.push( ...sideRules ); + } + + return rules; +} diff --git a/packages/style-engine/src/test/index.js b/packages/style-engine/src/test/index.js new file mode 100644 index 00000000000000..8eaf997c26324a --- /dev/null +++ b/packages/style-engine/src/test/index.js @@ -0,0 +1,32 @@ +/** + * Internal dependencies + */ +import { generate } from '../index'; + +describe( 'generate', () => { + it( 'should generate empty style', () => { + expect( generate( {}, '.some-selector' ) ).toEqual( '' ); + } ); + + it( 'should generate padding styles', () => { + expect( + generate( + { + spacing: { padding: '10px' }, + }, + '.some-selector' + ) + ).toEqual( '.some-selector { padding: 10px; }' ); + + expect( + generate( + { + spacing: { padding: { top: '10px', bottom: '5px' } }, + }, + '.some-selector' + ) + ).toEqual( + '.some-selector { padding-top: 10px; padding-bottom: 5px; }' + ); + } ); +} ); diff --git a/packages/style-engine/src/types.ts b/packages/style-engine/src/types.ts new file mode 100644 index 00000000000000..96dc5b41d036e1 --- /dev/null +++ b/packages/style-engine/src/types.ts @@ -0,0 +1,32 @@ +export type Box = { + top?: string; + bottom?: string; + left?: string; + right?: string; +}; + +export interface Style { + spacing?: { + padding?: string | Box; + margin?: string | Box; + lineHeight?: string; + fontSize?: string; + fontFamily?: string; + fontWeight?: string; + fontStyle?: string; + textDecoration?: string; + textTransform?: string; + letterSpacing?: string; + }; +} + +export type GeneratedCSSRule = { + selector: string; + value: string; + key: string; +}; + +export interface StyleDefinition { + name: string; + generate: ( style: Style, selector: string ) => GeneratedCSSRule[]; +} diff --git a/packages/style-engine/tsconfig.json b/packages/style-engine/tsconfig.json new file mode 100644 index 00000000000000..3c2c31f506f132 --- /dev/null +++ b/packages/style-engine/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "declarationDir": "build-types" + }, + "include": [ "src/**/*" ] +} diff --git a/tools/webpack/packages.js b/tools/webpack/packages.js index 48902ee90406a5..5aa5bcc74bb9d9 100644 --- a/tools/webpack/packages.js +++ b/tools/webpack/packages.js @@ -19,7 +19,11 @@ const { dependencies } = require( '../../package' ); const { baseConfig, plugins, stylesTransform } = require( './shared' ); const WORDPRESS_NAMESPACE = '@wordpress/'; -const BUNDLED_PACKAGES = [ '@wordpress/icons', '@wordpress/interface' ]; +const BUNDLED_PACKAGES = [ + '@wordpress/icons', + '@wordpress/interface', + '@wordpress/style-engine', +]; const gutenbergPackages = Object.keys( dependencies ) .filter(