Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experiment: Style Engine Rules & Store objects #42222

Merged
merged 17 commits into from
Jul 15, 2022
Merged

Conversation

aristath
Copy link
Member

@aristath aristath commented Jul 7, 2022

What?

This is a proof of concept, experimenting with possible implementations for the future of the Style Engine and how we may structure things.

This PR adds 2 objects:

  • WP_Style_Engine_CSS_Rule
  • WP_Style_Engine_CSS_Rules_Store

Reading the comments in other tickets, I think we'll need to create an object for rules, and allow them to be nested to accommodate child elements, pseudo-selectors etc.

The WP_Style_Engine_CSS_Rule object

This object holds a selector, the parent (if one exists), and the declarations, along with selector-specific methods.

The WP_Style_Engine_CSS_Rules_Store object

This object holds a collection of WP_Style_Engine_CSS_Rule objects by selector.

Usage

$rule = WP_Style_Engine_CSS_Rules_Store_Gutenberg::get_rule( '.wp-block-button a:hover' );
$rule->set_declarations(
	new WP_Style_Engine_CSS_Declarations_Gutenberg(
		array(
			'color'            => 'var(--block-button-link-hover-color)',
			'outline'          => '1px solid var(--block-button-link-hover-color)',
			'background-color' => 'black',
		)
	)
);
$css = $rule->get_css();
// string(97) " .wp-block-button a:hover {color: var(--block-button-link-hover-color); background-color: black;}"

The above will create 3 WP_Style_Engine_CSS_Rule objects, one for each of these selectors:

  • .wp-block-button
  • .wp-block-button a
  • .wp-block-button a:hover

The benefit of this structure is that this way, we can know which children - if any - a selector has, we can assign style-declarations on each of them, we can add & remove children and so on.
CSS becomes hierarchical and reflected in the objects structure. We can then process them as we see fit

Unit tests

npm run test-unit-php /var/www/html/wp-content/plugins/gutenberg/packages/style-engine/phpunit/class-wp-style-engine-css-rule-test.php
npm run test-unit-php /var/www/html/wp-content/plugins/gutenberg/packages/style-engine/phpunit/class-wp-style-engine-css-rules-store-test.php

@aristath aristath requested a review from ramonjd July 7, 2022 10:12
@github-actions
Copy link

github-actions bot commented Jul 7, 2022

Size Change: +500 B (0%)

Total Size: 1.26 MB

Filename Size Change
build/block-editor/index.min.js 153 kB +123 B (0%)
build/block-library/blocks/cover/style-rtl.css 1.55 kB -1 B (0%)
build/block-library/blocks/cover/style.css 1.55 kB -1 B (0%)
build/block-library/style.css 11.7 kB +1 B (0%)
build/blocks/index.min.js 47.1 kB +103 B (0%)
build/edit-site/index.min.js 53.6 kB +275 B (+1%)
ℹ️ View Unchanged
Filename Size
build/a11y/index.min.js 982 B
build/annotations/index.min.js 2.76 kB
build/api-fetch/index.min.js 2.26 kB
build/autop/index.min.js 2.14 kB
build/blob/index.min.js 475 B
build/block-directory/index.min.js 6.58 kB
build/block-directory/style-rtl.css 990 B
build/block-directory/style.css 991 B
build/block-editor/default-editor-styles-rtl.css 378 B
build/block-editor/default-editor-styles.css 378 B
build/block-editor/style-rtl.css 14.6 kB
build/block-editor/style.css 14.6 kB
build/block-library/blocks/archives/editor-rtl.css 61 B
build/block-library/blocks/archives/editor.css 60 B
build/block-library/blocks/archives/style-rtl.css 65 B
build/block-library/blocks/archives/style.css 65 B
build/block-library/blocks/audio/editor-rtl.css 150 B
build/block-library/blocks/audio/editor.css 150 B
build/block-library/blocks/audio/style-rtl.css 103 B
build/block-library/blocks/audio/style.css 103 B
build/block-library/blocks/audio/theme-rtl.css 110 B
build/block-library/blocks/audio/theme.css 110 B
build/block-library/blocks/avatar/editor-rtl.css 116 B
build/block-library/blocks/avatar/editor.css 116 B
build/block-library/blocks/avatar/style-rtl.css 59 B
build/block-library/blocks/avatar/style.css 59 B
build/block-library/blocks/block/editor-rtl.css 161 B
build/block-library/blocks/block/editor.css 161 B
build/block-library/blocks/button/editor-rtl.css 441 B
build/block-library/blocks/button/editor.css 441 B
build/block-library/blocks/button/style-rtl.css 543 B
build/block-library/blocks/button/style.css 543 B
build/block-library/blocks/buttons/editor-rtl.css 292 B
build/block-library/blocks/buttons/editor.css 292 B
build/block-library/blocks/buttons/style-rtl.css 275 B
build/block-library/blocks/buttons/style.css 275 B
build/block-library/blocks/calendar/style-rtl.css 207 B
build/block-library/blocks/calendar/style.css 207 B
build/block-library/blocks/categories/editor-rtl.css 84 B
build/block-library/blocks/categories/editor.css 83 B
build/block-library/blocks/categories/style-rtl.css 79 B
build/block-library/blocks/categories/style.css 79 B
build/block-library/blocks/code/style-rtl.css 103 B
build/block-library/blocks/code/style.css 103 B
build/block-library/blocks/code/theme-rtl.css 124 B
build/block-library/blocks/code/theme.css 124 B
build/block-library/blocks/columns/editor-rtl.css 108 B
build/block-library/blocks/columns/editor.css 108 B
build/block-library/blocks/columns/style-rtl.css 406 B
build/block-library/blocks/columns/style.css 406 B
build/block-library/blocks/comment-author-avatar/editor-rtl.css 125 B
build/block-library/blocks/comment-author-avatar/editor.css 125 B
build/block-library/blocks/comment-content/style-rtl.css 92 B
build/block-library/blocks/comment-content/style.css 92 B
build/block-library/blocks/comment-template/style-rtl.css 187 B
build/block-library/blocks/comment-template/style.css 185 B
build/block-library/blocks/comments-pagination-numbers/editor-rtl.css 123 B
build/block-library/blocks/comments-pagination-numbers/editor.css 121 B
build/block-library/blocks/comments-pagination/editor-rtl.css 222 B
build/block-library/blocks/comments-pagination/editor.css 209 B
build/block-library/blocks/comments-pagination/style-rtl.css 235 B
build/block-library/blocks/comments-pagination/style.css 231 B
build/block-library/blocks/comments-title/editor-rtl.css 75 B
build/block-library/blocks/comments-title/editor.css 75 B
build/block-library/blocks/comments/editor-rtl.css 95 B
build/block-library/blocks/comments/editor.css 95 B
build/block-library/blocks/cover/editor-rtl.css 615 B
build/block-library/blocks/cover/editor.css 616 B
build/block-library/blocks/embed/editor-rtl.css 293 B
build/block-library/blocks/embed/editor.css 293 B
build/block-library/blocks/embed/style-rtl.css 410 B
build/block-library/blocks/embed/style.css 410 B
build/block-library/blocks/embed/theme-rtl.css 110 B
build/block-library/blocks/embed/theme.css 110 B
build/block-library/blocks/file/editor-rtl.css 300 B
build/block-library/blocks/file/editor.css 300 B
build/block-library/blocks/file/style-rtl.css 253 B
build/block-library/blocks/file/style.css 254 B
build/block-library/blocks/file/view.min.js 346 B
build/block-library/blocks/freeform/editor-rtl.css 2.44 kB
build/block-library/blocks/freeform/editor.css 2.44 kB
build/block-library/blocks/gallery/editor-rtl.css 948 B
build/block-library/blocks/gallery/editor.css 950 B
build/block-library/blocks/gallery/style-rtl.css 1.53 kB
build/block-library/blocks/gallery/style.css 1.53 kB
build/block-library/blocks/gallery/theme-rtl.css 108 B
build/block-library/blocks/gallery/theme.css 108 B
build/block-library/blocks/group/editor-rtl.css 333 B
build/block-library/blocks/group/editor.css 333 B
build/block-library/blocks/group/style-rtl.css 57 B
build/block-library/blocks/group/style.css 57 B
build/block-library/blocks/group/theme-rtl.css 78 B
build/block-library/blocks/group/theme.css 78 B
build/block-library/blocks/heading/style-rtl.css 76 B
build/block-library/blocks/heading/style.css 76 B
build/block-library/blocks/html/editor-rtl.css 327 B
build/block-library/blocks/html/editor.css 329 B
build/block-library/blocks/image/editor-rtl.css 736 B
build/block-library/blocks/image/editor.css 737 B
build/block-library/blocks/image/style-rtl.css 627 B
build/block-library/blocks/image/style.css 630 B
build/block-library/blocks/image/theme-rtl.css 110 B
build/block-library/blocks/image/theme.css 110 B
build/block-library/blocks/latest-comments/style-rtl.css 284 B
build/block-library/blocks/latest-comments/style.css 284 B
build/block-library/blocks/latest-posts/editor-rtl.css 199 B
build/block-library/blocks/latest-posts/editor.css 198 B
build/block-library/blocks/latest-posts/style-rtl.css 463 B
build/block-library/blocks/latest-posts/style.css 462 B
build/block-library/blocks/list/style-rtl.css 88 B
build/block-library/blocks/list/style.css 88 B
build/block-library/blocks/media-text/editor-rtl.css 266 B
build/block-library/blocks/media-text/editor.css 263 B
build/block-library/blocks/media-text/style-rtl.css 493 B
build/block-library/blocks/media-text/style.css 490 B
build/block-library/blocks/more/editor-rtl.css 431 B
build/block-library/blocks/more/editor.css 431 B
build/block-library/blocks/navigation-link/editor-rtl.css 705 B
build/block-library/blocks/navigation-link/editor.css 703 B
build/block-library/blocks/navigation-link/style-rtl.css 115 B
build/block-library/blocks/navigation-link/style.css 115 B
build/block-library/blocks/navigation-submenu/editor-rtl.css 296 B
build/block-library/blocks/navigation-submenu/editor.css 295 B
build/block-library/blocks/navigation-submenu/view.min.js 423 B
build/block-library/blocks/navigation/editor-rtl.css 2.03 kB
build/block-library/blocks/navigation/editor.css 2.04 kB
build/block-library/blocks/navigation/style-rtl.css 1.96 kB
build/block-library/blocks/navigation/style.css 1.95 kB
build/block-library/blocks/navigation/view-modal.min.js 2.78 kB
build/block-library/blocks/navigation/view.min.js 443 B
build/block-library/blocks/nextpage/editor-rtl.css 395 B
build/block-library/blocks/nextpage/editor.css 395 B
build/block-library/blocks/page-list/editor-rtl.css 363 B
build/block-library/blocks/page-list/editor.css 363 B
build/block-library/blocks/page-list/style-rtl.css 175 B
build/block-library/blocks/page-list/style.css 175 B
build/block-library/blocks/paragraph/editor-rtl.css 157 B
build/block-library/blocks/paragraph/editor.css 157 B
build/block-library/blocks/paragraph/style-rtl.css 260 B
build/block-library/blocks/paragraph/style.css 260 B
build/block-library/blocks/post-author/style-rtl.css 175 B
build/block-library/blocks/post-author/style.css 176 B
build/block-library/blocks/post-comments-form/editor-rtl.css 96 B
build/block-library/blocks/post-comments-form/editor.css 96 B
build/block-library/blocks/post-comments-form/style-rtl.css 495 B
build/block-library/blocks/post-comments-form/style.css 495 B
build/block-library/blocks/post-comments/editor-rtl.css 77 B
build/block-library/blocks/post-comments/editor.css 77 B
build/block-library/blocks/post-comments/style-rtl.css 632 B
build/block-library/blocks/post-comments/style.css 630 B
build/block-library/blocks/post-excerpt/editor-rtl.css 73 B
build/block-library/blocks/post-excerpt/editor.css 73 B
build/block-library/blocks/post-excerpt/style-rtl.css 69 B
build/block-library/blocks/post-excerpt/style.css 69 B
build/block-library/blocks/post-featured-image/editor-rtl.css 605 B
build/block-library/blocks/post-featured-image/editor.css 605 B
build/block-library/blocks/post-featured-image/style-rtl.css 153 B
build/block-library/blocks/post-featured-image/style.css 153 B
build/block-library/blocks/post-template/editor-rtl.css 99 B
build/block-library/blocks/post-template/editor.css 98 B
build/block-library/blocks/post-template/style-rtl.css 282 B
build/block-library/blocks/post-template/style.css 282 B
build/block-library/blocks/post-terms/style-rtl.css 73 B
build/block-library/blocks/post-terms/style.css 73 B
build/block-library/blocks/post-title/style-rtl.css 80 B
build/block-library/blocks/post-title/style.css 80 B
build/block-library/blocks/preformatted/style-rtl.css 103 B
build/block-library/blocks/preformatted/style.css 103 B
build/block-library/blocks/pullquote/editor-rtl.css 198 B
build/block-library/blocks/pullquote/editor.css 198 B
build/block-library/blocks/pullquote/style-rtl.css 370 B
build/block-library/blocks/pullquote/style.css 370 B
build/block-library/blocks/pullquote/theme-rtl.css 167 B
build/block-library/blocks/pullquote/theme.css 167 B
build/block-library/blocks/query-pagination-numbers/editor-rtl.css 122 B
build/block-library/blocks/query-pagination-numbers/editor.css 121 B
build/block-library/blocks/query-pagination/editor-rtl.css 221 B
build/block-library/blocks/query-pagination/editor.css 211 B
build/block-library/blocks/query-pagination/style-rtl.css 234 B
build/block-library/blocks/query-pagination/style.css 231 B
build/block-library/blocks/query/editor-rtl.css 365 B
build/block-library/blocks/query/editor.css 364 B
build/block-library/blocks/quote/style-rtl.css 213 B
build/block-library/blocks/quote/style.css 213 B
build/block-library/blocks/quote/theme-rtl.css 223 B
build/block-library/blocks/quote/theme.css 226 B
build/block-library/blocks/read-more/style-rtl.css 132 B
build/block-library/blocks/read-more/style.css 132 B
build/block-library/blocks/rss/editor-rtl.css 202 B
build/block-library/blocks/rss/editor.css 204 B
build/block-library/blocks/rss/style-rtl.css 289 B
build/block-library/blocks/rss/style.css 288 B
build/block-library/blocks/search/editor-rtl.css 165 B
build/block-library/blocks/search/editor.css 165 B
build/block-library/blocks/search/style-rtl.css 385 B
build/block-library/blocks/search/style.css 386 B
build/block-library/blocks/search/theme-rtl.css 114 B
build/block-library/blocks/search/theme.css 114 B
build/block-library/blocks/separator/editor-rtl.css 146 B
build/block-library/blocks/separator/editor.css 146 B
build/block-library/blocks/separator/style-rtl.css 233 B
build/block-library/blocks/separator/style.css 233 B
build/block-library/blocks/separator/theme-rtl.css 194 B
build/block-library/blocks/separator/theme.css 194 B
build/block-library/blocks/shortcode/editor-rtl.css 464 B
build/block-library/blocks/shortcode/editor.css 464 B
build/block-library/blocks/site-logo/editor-rtl.css 708 B
build/block-library/blocks/site-logo/editor.css 708 B
build/block-library/blocks/site-logo/style-rtl.css 192 B
build/block-library/blocks/site-logo/style.css 192 B
build/block-library/blocks/site-tagline/editor-rtl.css 86 B
build/block-library/blocks/site-tagline/editor.css 86 B
build/block-library/blocks/site-title/editor-rtl.css 84 B
build/block-library/blocks/site-title/editor.css 84 B
build/block-library/blocks/social-link/editor-rtl.css 177 B
build/block-library/blocks/social-link/editor.css 177 B
build/block-library/blocks/social-links/editor-rtl.css 674 B
build/block-library/blocks/social-links/editor.css 673 B
build/block-library/blocks/social-links/style-rtl.css 1.39 kB
build/block-library/blocks/social-links/style.css 1.38 kB
build/block-library/blocks/spacer/editor-rtl.css 322 B
build/block-library/blocks/spacer/editor.css 322 B
build/block-library/blocks/spacer/style-rtl.css 48 B
build/block-library/blocks/spacer/style.css 48 B
build/block-library/blocks/table/editor-rtl.css 494 B
build/block-library/blocks/table/editor.css 494 B
build/block-library/blocks/table/style-rtl.css 611 B
build/block-library/blocks/table/style.css 609 B
build/block-library/blocks/table/theme-rtl.css 175 B
build/block-library/blocks/table/theme.css 175 B
build/block-library/blocks/tag-cloud/style-rtl.css 226 B
build/block-library/blocks/tag-cloud/style.css 227 B
build/block-library/blocks/template-part/editor-rtl.css 178 B
build/block-library/blocks/template-part/editor.css 178 B
build/block-library/blocks/template-part/theme-rtl.css 101 B
build/block-library/blocks/template-part/theme.css 101 B
build/block-library/blocks/text-columns/editor-rtl.css 95 B
build/block-library/blocks/text-columns/editor.css 95 B
build/block-library/blocks/text-columns/style-rtl.css 166 B
build/block-library/blocks/text-columns/style.css 166 B
build/block-library/blocks/verse/style-rtl.css 87 B
build/block-library/blocks/verse/style.css 87 B
build/block-library/blocks/video/editor-rtl.css 561 B
build/block-library/blocks/video/editor.css 563 B
build/block-library/blocks/video/style-rtl.css 159 B
build/block-library/blocks/video/style.css 159 B
build/block-library/blocks/video/theme-rtl.css 110 B
build/block-library/blocks/video/theme.css 110 B
build/block-library/common-rtl.css 987 B
build/block-library/common.css 984 B
build/block-library/editor-rtl.css 10.2 kB
build/block-library/editor.css 10.2 kB
build/block-library/elements-rtl.css 54 B
build/block-library/elements.css 54 B
build/block-library/index.min.js 184 kB
build/block-library/reset-rtl.css 478 B
build/block-library/reset.css 478 B
build/block-library/style-rtl.css 11.7 kB
build/block-library/theme-rtl.css 695 B
build/block-library/theme.css 700 B
build/block-serialization-default-parser/index.min.js 1.11 kB
build/block-serialization-spec-parser/index.min.js 2.83 kB
build/components/index.min.js 230 kB
build/components/style-rtl.css 14 kB
build/components/style.css 14.1 kB
build/compose/index.min.js 11.7 kB
build/core-data/index.min.js 14.7 kB
build/customize-widgets/index.min.js 11.2 kB
build/customize-widgets/style-rtl.css 1.4 kB
build/customize-widgets/style.css 1.4 kB
build/data-controls/index.min.js 653 B
build/data/index.min.js 7.99 kB
build/date/index.min.js 32 kB
build/deprecated/index.min.js 507 B
build/dom-ready/index.min.js 324 B
build/dom/index.min.js 4.69 kB
build/edit-navigation/index.min.js 16 kB
build/edit-navigation/style-rtl.css 4.02 kB
build/edit-navigation/style.css 4.03 kB
build/edit-post/classic-rtl.css 546 B
build/edit-post/classic.css 547 B
build/edit-post/index.min.js 30.5 kB
build/edit-post/style-rtl.css 6.97 kB
build/edit-post/style.css 6.97 kB
build/edit-site/style-rtl.css 8.23 kB
build/edit-site/style.css 8.22 kB
build/edit-widgets/index.min.js 16.5 kB
build/edit-widgets/style-rtl.css 4.35 kB
build/edit-widgets/style.css 4.35 kB
build/editor/index.min.js 41.3 kB
build/editor/style-rtl.css 3.65 kB
build/editor/style.css 3.65 kB
build/element/index.min.js 4.27 kB
build/escape-html/index.min.js 537 B
build/format-library/index.min.js 6.75 kB
build/format-library/style-rtl.css 571 B
build/format-library/style.css 571 B
build/hooks/index.min.js 1.64 kB
build/html-entities/index.min.js 448 B
build/i18n/index.min.js 3.77 kB
build/is-shallow-equal/index.min.js 527 B
build/keyboard-shortcuts/index.min.js 1.78 kB
build/keycodes/index.min.js 1.38 kB
build/list-reusable-blocks/index.min.js 1.74 kB
build/list-reusable-blocks/style-rtl.css 835 B
build/list-reusable-blocks/style.css 835 B
build/media-utils/index.min.js 2.93 kB
build/notices/index.min.js 953 B
build/nux/index.min.js 2.05 kB
build/nux/style-rtl.css 732 B
build/nux/style.css 728 B
build/plugins/index.min.js 1.94 kB
build/preferences-persistence/index.min.js 2.22 kB
build/preferences/index.min.js 1.3 kB
build/primitives/index.min.js 933 B
build/priority-queue/index.min.js 612 B
build/react-i18n/index.min.js 696 B
build/react-refresh-entry/index.min.js 8.44 kB
build/react-refresh-runtime/index.min.js 7.31 kB
build/redux-routine/index.min.js 2.68 kB
build/reusable-blocks/index.min.js 2.22 kB
build/reusable-blocks/style-rtl.css 256 B
build/reusable-blocks/style.css 256 B
build/rich-text/index.min.js 11.1 kB
build/server-side-render/index.min.js 1.61 kB
build/shortcode/index.min.js 1.53 kB
build/token-list/index.min.js 644 B
build/url/index.min.js 3.61 kB
build/vendors/react-dom.min.js 38.5 kB
build/vendors/react.min.js 4.34 kB
build/viewport/index.min.js 1.08 kB
build/warning/index.min.js 268 B
build/widgets/index.min.js 7.19 kB
build/widgets/style-rtl.css 1.16 kB
build/widgets/style.css 1.16 kB
build/wordcount/index.min.js 1.06 kB

compressed-size-action

@ramonjd
Copy link
Member

ramonjd commented Jul 8, 2022

Thank you for working on this! I ran out of time to test it out today, but will get to it ASAP 🙇 Just wanted to say thanks 🙏

@ramonjd
Copy link
Member

ramonjd commented Jul 11, 2022

The benefit of this structure is that this way, we can know which children - if any - a selector has, we can assign style-declarations on each of them, we can add & remove children and so on.
CSS becomes hierarchical and reflected in the objects structure. We can then process them as we see fit

I love the concept!

I know support might be a looooong way off, but do you think we'd also be able to leverage this system for CSS nesting? It'd be awesome one to offer it as an opt-in.

Copy link
Contributor

@andrewserong andrewserong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 this is a very cool exploration @aristath! I like the hierarchical structure, it feels like this will enable a lot of flexibility when it comes to optimising output 👍

A couple of questions:

  • WIth the nested structure, will we ever run into an issue where we need a flat list of all selectors for de-duping, etc, and needing to flatten the data structure first might make it harder? I don't think so personally, but just curious if there are any downsides to storing a more nested version of rules within rules when it comes to optimisation.
  • What are the potential trade-offs between the static $rules property of the store, and having it an object instance property? Apologies if my OOP naming is slightly off here (😅), to be more verbose: it looks like in this PR we assume a single store exists for the lifecycle of the current request. Will there be a case where we need to have two separate stores, even as an interim step? I imagine the goal is that ideally for a single request we only ever have a single store, but I was curious if it makes it easier or harder if we have a single store when it comes to using the style engine in global styles and block supports at the same time. For example, if incrementally we use the style engine to improve the styles that get output to the global stylesheet, and separately for the styles that are output to style tags for individual blocks at render time. Eventually it'd be great for these to be in the one sheet, but in incremental steps, it might be easier if they're separate. Just a thought, though, as I know both you and @ramonjd have been doing more exploration into the global styles implications recently!

$parts = array_map( 'trim', $parts );

// Remove empty parts.
$parts = array_filter( $parts );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice that you're handling accidental double-spaces in the selector 👍

$parts = array_filter( $parts );

// Split parts on ':' characters.
$parts = static::deconstruct_selector_array_on_character( $parts, ':' );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to factor in double-colons, too? I doubt we'll be doing too much with pseudo elements to begin with, but just in case we need to implement it at some point (e.g. ::first-letter)?

@ramonjd
Copy link
Member

ramonjd commented Jul 12, 2022

I imagine the goal is that ideally for a single request we only ever have a single store, but I was curious if it makes it easier or harder if we have a single store when it comes to using the style engine in global styles and block supports at the same time.

A single store with no frills, that is, one that just stores selectors => rules would be a good place to kick things off in my opinion, something we can build upon as we start to toy around with pre-processing.

With so many bespoke features being added to WP_Theme_JSON and, not as frequently, the block.json structure, it's hard to think ahead and not be reactive, but we're kind of like playing ping pong against an octopus.

I definitely like the thrust of this PR and we'll probably need to structure the store/and or its contents to make certain pre-processing tasks easier, especially when we want to experiment with de-duping and whatever.

Also theme.json probably will, and probably should, be doing a lot of the work for us in terms of cascade and inheritance, and even nesting. So we should try to leverage that if it all possible when building the rules.

For example, the pseudo elements model introduced in #41786

{
    "blocks": {
        "core/group": {
             "elements": {
                "link": {
                    "color": {
                        "text": "green"
                     },
                    ":hover": {
                        "color": {
                            "text": "red"
                        }
                    }
                }
            }
        }
    }
}

gives us all the information we need to build the following selectors:

.wp-block-group a {
    color: green;
    &:hover {
        color: red;
    }
}

@andrewserong
Copy link
Contributor

Thanks for adding the extra thoughts, Ramon!

we're kind of like playing ping pong against an octopus.

Oh, I hear that! 🤣

A single store with no frills, that is, one that just stores selectors => rules would be a good place to kick things off in my opinion, something we can build upon as we start to toy around with pre-processing.

Good point, and perhaps the restriction of only having a single store will help avoid accidentally adding additional complexity that could then be hard to wind back if we had multiple stores floating around? After all, with the goal of having a single de-duped output in a single style tag or stylesheet, having a single store makes sense to me 👍

@aristath
Copy link
Member Author

@ramonjd

I know support might be a looooong way off, but do you think we'd also be able to leverage this system for CSS nesting? It'd be awesome one to offer it as an opt-in.

Definitely! If we have the structure in place with hierarchical objects for selectors, then all we'd have to do is to change the method that generates the styles.

@andrewserong

WIth the nested structure, will we ever run into an issue where we need a flat list of all selectors for de-duping, etc, and needing to flatten the data structure first might make it harder? I don't think so personally, but just curious if there are any downsides to storing a more nested version of rules within rules when it comes to optimisation.

Yes, having a flat list of all selectors would be needed. That's why the WP_Style_Engine_CSS_Rules_Store::$rules prop actually contains a flat array of all selectors already 😉
The array contains key => value pairs where the value is an object, BUT the key is the full selector.
So the array would look like this:

[
  '.wp-block-button'         => Obj,
  '.wp-block-button a'       => Obj,
  '.wp-block-button a:hover' => Obj,
]

The get_css() method on each of the objects of the array only gets the styles for that specific object (and not for the whole tree with children etc).

it looks like in this PR we assume a single store exists for the lifecycle of the current request. Will there be a case where we need to have two separate stores, even as an interim step? I imagine the goal is that ideally for a single request we only ever have a single store, but I was curious if it makes it easier or harder if we have a single store when it comes to using the style engine in global styles and block supports at the same time. For example, if incrementally we use the style engine to improve the styles that get output to the global stylesheet, and separately for the styles that are output to style tags for individual blocks at render time. Eventually it'd be great for these to be in the one sheet, but in incremental steps, it might be easier if they're separate.

You're right, in this implementation there's a single store.
However, converting the class to allow multiple named instances would be trivial. So we could do something like WP_Style_Engine_CSS_Rules_Store::get_instance( 'global-styles' ); for example, and have multiple isolated instances of the object. That would probably allow for maximum versatility in the future as we'll need definitely need to create separate styles and some will go in the head, some may go in the footer and so on.

Also theme.json probably will, and probably should, be doing a lot of the work for us in terms of cascade and inheritance, and even nesting. So we should try to leverage that if it all possible when building the rules.

Definitely, +1!


There are a lot of things that can and should be improved, but before I dedicate more time to this I wanted to hear what the 2 of you think and if this is direction we want to take with the style engine.
Does it make sense to you to have a store? Does it make sense to have a rule object, and should we allow them to be hierarchical/nested like this?
I was trying to imagine scenarios and things we'll need to do in the future (like the CSS nesting that Ramon mentioned above) and so on.
Nesting would also allow for bespoke implementations on larger sites, where they may even choose to do something like get the list of hierarchical objects, and create a custom compiler/caching implementation for them. I feel it's just more open-ended this way and has the potential to take us further into the future for a few years 👍

@ramonjd
Copy link
Member

ramonjd commented Jul 13, 2022

There are a lot of things that can and should be improved, but before I dedicate more time to this I wanted to hear what the 2 of you think and if this is direction we want to take with the style engine.

Personally, I think recording rule hierarchy is something that'll make processing decisions easier down the track. I love these experiments - they're really forward thinking.

Not necessarily now, but it'd be interesting to test some processing scenarios, and think about how deeply nested we'd allow things to be to control recursion and performance. We might come up with a different approach as a result 🤷

Some unit tests would be illustrative in order to get a better understanding of how things might be integrated into the flow.

As far as the store model and processing, I'd say time is on our side. For 6.1 it'd be nice to have a super, super simple store in place, one that can be extended with the type of functionality that exists in this PR.

Does it make sense to you to have a store?

YES! 🕺

I was chatting with @andrewserong and it sounds like the store is the next major component we'll need to unblock the next round of shorter term goals, namely, reducing the amount of style tags and building a global styles stylesheet.

Seeing as processing is a bit further down the track, and we won't need to store a parent->children relationship, do you think it's feasible to reduce the scope of our store here? I mean to say, keep a rule object and a rule store, but work on the more complex selector parsing and hierarchical storage in another, later PR?

That way we'd have time to test things with real-world styles, come up with way to interact with the store model, react to new feature grenades, and basically not commit to a feature before there's a need for it.

Given that the style engine has complete control of parsing/processing/storage, and it all happens at runtime, I think it'd be safe to iterate, i.e., we're not going to cause backwards compatibility issues.

For preset purposes, I'm not 100% sure yet, but I'm thinking about a class that we could instantiate for each store we'd need, e.g., one for block supports and one for global styles with key/value pairs (selector => rules) and nothing else.

Maybe down the road, in the land of unicorns 🦄 , we'd end up with one store for everything!

Does it make sense to have a rule object

Yeah, similar to what we now have with WP_Style_Engine_CSS_Declarations I can imagine that'd it'd be useful to have an CSS rule object that can print out its own CSS rule string, control its own formatting etc.

An example might be a prettify option where we'd pass a level prop to the class and it would determine the number of idents (for nesting).

and should we allow themto be hierarchical/nested like this?

As mentioned, I reckon it's a great idea, and one we'll likely need.

I'm a bit of a slow poke though, and would love to see a base store, something as simple as we can make it, and then iterate towards the hierarchical/nested model with test cases. It'd give us time (of which we have plenty) to discuss and performance test and so on.

It's just a personal preference, but I think my attitude comes from having had things change under my feet so often, I'd hate for us to commit to one thing and then have to rearchitect a bunch of stuff, thereby losing all the hard work we've done.

🍺

So the array would look like this:

[
  '.wp-block-button'         => Obj,
  '.wp-block-button a'       => Obj,
  '.wp-block-button a:hover' => Obj,
]

Just curious, would we still store .wp-block-button even though it might not have any rules associated with it?

I have the case in mind where a block in theme.json declares styles for elements.link only (so .wp-block-button a and maybe .wp-block-button a:hover)

Or is it just for possible nesting scenario?

@ramonjd ramonjd added [Package] Style Engine /packages/style-engine [Type] Experimental Experimental feature or API. labels Jul 13, 2022
@andrewserong
Copy link
Contributor

Thanks for all the extra context @aristath, this is just a short comment to say that I think the ideas here are worth exploring further (and thanks for explaining the flat list of selector keys in the store 👍). As Ramon mentioned, I think the Store will come in very handy as we iteratively look at introducing the style engine to global styles, and the structure here is looking really appealing to me so far! I sometimes find these issues difficult to tease apart in my head in the abstract, but when looking at concrete tasks, the features seem to reveal themselves a bit better (like the use of the Declarations class in #42366 🎉). So from my perspective I think it's very much worth digging in a little further here to see how it might work!

@aristath
Copy link
Member Author

aristath commented Jul 13, 2022

Thank you @ramonjd and @andrewserong for the comments!
I took that feedback and pushed a few commits:

  • Removed the hierarchy from Rules objects, because it's not something that we need immediately, it can be done at a future date when the need arises
  • Converted the Store object to allow for multiple named instances. Each store is now independent and holds its own rules objects.

A usage example would look like this now with these changes:

// Get a named store.
$store = WP_Style_Engine_CSS_Rules_Store_Gutenberg::get_store( 'global-styles' );
// Get the rule object for a selector.
$rule = $store->get_rule( '.wp-block-button a:hover' );
// Set the declarations for the rule.
$rule->set_declarations(
	// Use the Declarations object. We can pass multiple declarations to the rule
	// at any time on a per-store basis.
	new WP_Style_Engine_CSS_Declarations_Gutenberg(
		array(
			'color'            => 'var(--block-button-link-hover-color)',
			'outline'          => '1px solid var(--block-button-link-hover-color)',
			'background-color' => 'black',
		)
	)
);
$css = $rule->get_css();
// string(96) ".wp-block-button a:hover {color: var(--block-button-link-hover-color); background-color: black;}"

The implementation is now a lot simpler since we got rid of the parent-child relations, and the objects are pretty minimal 🎉

@andrewserong
Copy link
Contributor

The implementation is now a lot simpler since we got rid of the parent-child relations, and the objects are pretty minimal

I love it, I think the simplicity here makes it a really solid addition that we can build on. I also really like the clarity of the APIs you've created between the Declarations class and the Rule class — the method for get_css() is so nice and clean 🎉

If we can add a few tests, I think this PR looks like it'll be good to merge!

Copy link
Member

@ramonjd ramonjd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this @aristath 🙇

I had to see it in action so I created some tests over at #42408 to test a few scenarios that we'll need.

Feel free to 100% ignore them if you had a different idea of implementation. It was just me trying to understand how this particular store as it stands would serve us.

The main requirement of a store right now is being able to register styles incrementally until a hook is fired, whereupon we'd want to fetch the store's entire contents in order to build a stylesheet.

Also I'm wondering about where CSS property merging would take place when adding CSS declarations, e.g., when we have 2 or more WP_Style_Engine_CSS_Declarations objects in a WP_Style_Engine_CSS_Rule with the same properties?

Should we merge before we store them? 🤔 If so, it might be easier for all classes to accept property => value declaration pairs, and return instances of WP_Style_Engine_CSS_Rule | WP_Style_Engine_CSS_Declarations in the get functions where required?

I'm not sure. Just asking.

It might be something that'll come out in the wash later too, so not a blocker. We can continue to iterate.

I'm happy to get this in as soon as we have tests and then carry on refining.

packages/style-engine/class-wp-style-engine-css-rule.php Outdated Show resolved Hide resolved
*
* @param string $selector The CSS selector.
*/
public function get_rule( $selector ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I'm missing something, we'll also need to add rules to stores from around Gutenberg.

When we ultimately want to enqueue a store's rules, then we'd want to retrieve them all at once.

Copy link
Member

@ramonjd ramonjd Jul 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah apologies, I think the get_rule threw me and I expected a corresponding set_rule.

The references penny didn't drop. 😄

It did eventually when I tried to write tests: https://github.com/WordPress/gutenberg/pull/42222/files 😮‍💨

So just to confirm we'd be doing something like this:

// Deal with incoming css rules from around Gutenberg.
$store_rule         = $new_pizza_store->get_rule( $selector );
$css_declarations   = new WP_Style_Engine_CSS_Declarations( $new_raw_declarations );
$store_rule->set_declarations( array( $css_declarations ) );

// Somewhere else... eventually.
$new_pizza_store->get_all_rules();

// Build the stylesheet from the rules...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point about getters vs setters and potential confusion. Perhaps using a separate setter could make the API a bit more explicit?

One of the things for us to keep in mind as we explore the style engine API is what sort of behaviour (and naming) we expect in WordPress APIs in general. So, for example, if the WP_Block_Type_Registry, WP_Block_Styles_Registry, and WP_Block_Patterns_Registry classes have separate register and get_registered methods (and get_registered returns null if the item is not registered), then consumers of this class might expect comparable behaviour / methods.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I found a use case for an explicit set_rule: https://github.com/WordPress/gutenberg/pull/42408/files#r920830003

Aside from the idea that it might be handy for people like me who don't like to think too hard 😆

Copy link
Member Author

@aristath aristath Jul 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, get_rule() is probably not the right name for this method.
What if we called it add_rule and also added a remove_rule method? 🤔
The method basically returns the rule if it already exists, or creates a new one if it doesn't.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add_rule and remove_rule work for me!


// Create the rule if it doesn't exist.
if ( empty( $this->rules[ $selector ] ) ) {
$this->rules[ $selector ] = new WP_Style_Engine_CSS_Rule( $selector );
Copy link
Member

@ramonjd ramonjd Jul 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would we always want to create and store a new rule here? Should it be explicit through a set_rule method or something like that?

It might end up creating unnecessary records. I could be missing something though!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see 🤦 This is the setter and getter.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah... it's a 2-in-1 method. Which is why we should probably rename it... 😅

@ramonjd
Copy link
Member

ramonjd commented Jul 14, 2022

If we can add a few tests, I think this PR looks like it'll be good to merge!

Ditto 🙇

@aristath
Copy link
Member Author

aristath commented Jul 14, 2022

Lots of amazing feedback here, thank you both!
I took that feedback and pushed the following changes:

  • WP_Style_Engine_CSS_Rule now accepts as $declarations either an array of declarations (property => value pairs), OR a WP_Style_Engine_CSS_Declarations object. That's both in the object's constructor, and the set_declarations() method.
  • WP_Style_Engine_CSS_Rule->declarations is now a single WP_Style_Engine_CSS_Declarations object instead of an array of objects. Since this is now a single object, any declarations we add (either as an array or a WP_Style_Engine_CSS_Declarations object) get merged in the same $declarations object. This takes care of overrides in the CSS (like adding color:red at some point and then later color:blue) and reduces the overall footprint of this implementation 🎉
  • Did a small tweak to allow chaining methods. This is simply for convenience, and also a performance improvement.

With the above changes, we could now write the code from my previous comment like this:

$css = WP_Style_Engine_CSS_Rules_Store_Gutenberg::get_store( 'global-styles' )
	->get_rule( '.wp-block-button a:hover' )
	->set_declarations(
		array(
			'color'            => 'var(--block-button-link-hover-color)',
			'outline'          => '1px solid var(--block-button-link-hover-color)',
			'background-color' => 'black',
		)
	)->get_css();
// string(96) ".wp-block-button a:hover {color: var(--block-button-link-hover-color); background-color: black;}"

@ramonjd Thank you for writing the tests on #42408!! Writing tests is not my strong suit, I have a (slowly shrinking) gap of knowledge there 😓
With these latest changes I think the tests should now pass, I'll push them to your branch and we'll see how that goes 👍

@ramonjd
Copy link
Member

ramonjd commented Jul 15, 2022

@aristath I've closed the separate tests PR and added them to this one. Hope you don't mind. Saves us looking after two PRs!

Copy link
Member

@ramonjd ramonjd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just doing a flyby ✅

This is once again awesome work. Things are starting to come together 🎉

Since we're not using these classes yet, I'd be fine with merging this (assuming tests pass) and doing the add_rule/set_rule/whatever follow up later, or adding them in this PR.

Up to you 👍

@ramonjd ramonjd marked this pull request as ready for review July 15, 2022 01:37
Copy link
Contributor

@andrewserong andrewserong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM too, and like @ramonjd mentions, we could either update get_rule to add_rule now, or in a follow-up PR, whichever way you prefer, since we're not yet using these classes anywhere 👍

Thanks for creating nicely scoped PRs, too, it's been a great way to contain the discussion to incremental pieces so we can pragmatically move the project forward, piece by piece 🎉

@aristath aristath merged commit 3136704 into trunk Jul 15, 2022
@aristath aristath deleted the add/style-engine/rules branch July 15, 2022 08:18
@github-actions github-actions bot added this to the Gutenberg 13.8 milestone Jul 15, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Package] Style Engine /packages/style-engine [Type] Experimental Experimental feature or API.
Projects
Status: 🏆 Done
Development

Successfully merging this pull request may close these issues.

3 participants