From 7b578b15a30fc49abcd226b950066799e0559816 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Mon, 2 Jan 2023 12:51:26 +0000 Subject: [PATCH] Merge changes published in the Gutenberg plugin "release/14.9" branch --- .eslintrc.js | 5 +- .github/CODEOWNERS | 1 - .github/workflows/end2end-test-playwright.yml | 66 - .github/workflows/end2end-test.yml | 97 +- .github/workflows/flaky-tests.yml | 38 - bin/api-docs/gen-theme-reference.js | 29 +- bin/plugin/commands/performance.js | 16 + changelog.txt | 643 +++ docs/README.md | 2 +- docs/contributors/code/release.md | 4 + docs/contributors/code/scripts.md | 1 - .../extending-the-query-loop-block.md | 6 + .../javascript/extending-the-block-editor.md | 2 +- docs/manifest.json | 12 - docs/reference-guides/README.md | 1 - .../block-api/block-deprecation.md | 12 +- .../block-api/block-metadata.md | 2 +- .../block-api/block-patterns.md | 3 + .../block-api/block-registration.md | 2 +- docs/reference-guides/core-blocks.md | 4 +- docs/reference-guides/data/README.md | 1 - docs/reference-guides/data/data-core-nux.md | 99 - .../reference-guides/filters/block-filters.md | 5 +- docs/reference-guides/slotfills/README.md | 4 +- .../plugin-sidebar-more-menu-item.md | 5 +- .../theme-json-reference/theme-json-living.md | 54 +- docs/toc.json | 1 - gutenberg.php | 2 +- lib/block-supports/typography.php | 46 +- ....php => class-wp-theme-json-gutenberg.php} | 3853 ++++++++++++----- ...class-wp-theme-json-resolver-gutenberg.php | 701 +++ lib/client-assets.php | 19 +- ...ss-gutenberg-rest-templates-controller.php | 2 +- .../class-wp-theme-json-resolver-6-1.php | 191 - lib/compat/wordpress-6.2/block-patterns.php | 3 +- .../wordpress-6.2/block-template-utils.php | 112 + ...erg-rest-block-patterns-controller-6-2.php | 122 + ...berg-rest-global-styles-controller-6-2.php | 61 +- .../wordpress-6.2/class-wp-theme-json-6-2.php | 391 -- .../class-wp-theme-json-resolver-6-2.php | 230 - .../get-global-styles-and-settings.php | 9 +- lib/compat/wordpress-6.2/rest-api.php | 15 + lib/compat/wordpress-6.2/script-loader.php | 27 +- lib/compat/wordpress-6.2/theme.php | 23 + lib/compat/wordpress-6.2/widgets.php | 32 + .../block-editor-settings-mobile.php | 2 +- ...-rest-block-editor-settings-controller.php | 6 + .../class-wp-theme-json-gutenberg.php | 31 - ...class-wp-theme-json-resolver-gutenberg.php | 177 - lib/experimental/class-wp-web-fonts.php | 744 ++++ .../class-wp-webfonts-provider-local.php | 20 +- .../class-wp-webfonts-provider.php | 70 +- lib/experimental/class-wp-webfonts-utils.php | 122 + lib/experimental/class-wp-webfonts.php | 467 +- lib/experimental/editor-settings.php | 3 + .../html/class-wp-html-tag-processor.php | 75 +- .../register-webfonts-from-theme-json.php | 271 +- lib/experimental/webfonts-deprecations.php | 100 + lib/experimental/webfonts.php | 229 +- lib/experiments-page.php | 16 + lib/load.php | 27 +- .../wordpress-6.1 => }/theme-i18n.json | 0 lib/{compat/wordpress-6.1 => }/theme.json | 14 + package-lock.json | 203 +- package.json | 5 +- packages/base-styles/_long-content-fade.scss | 61 + packages/base-styles/_mixins.scss | 64 +- packages/base-styles/_variables.scss | 2 +- packages/base-styles/_z-index.scss | 12 +- .../test/index.js | 2 + packages/block-editor/CHANGELOG.md | 8 +- packages/block-editor/README.md | 3 +- .../alignment-control/test/index.js | 2 + .../src/components/alignment-control/ui.js | 8 +- .../block-alignment-control/test/index.js | 2 + .../use-available-alignments.js | 7 +- .../src/components/block-icon/index.js | 6 +- .../src/components/block-icon/test/index.js | 14 +- .../src/components/block-inspector/index.js | 81 +- .../src/components/block-inspector/style.scss | 7 + .../components/block-list-appender/index.js | 119 +- .../block-list-appender/index.native.js | 79 +- .../block-actions-menu.native.js | 40 +- .../block-mobile-toolbar/index.native.js | 2 +- .../block-actions-menu.native.js.snap | 125 + .../test/block-actions-menu.native.js | 439 ++ .../test/__snapshots__/index.native.js.snap | 42 + .../block-mover/test/index.native.js | 158 +- .../components/block-pattern-setup/index.js | 21 +- .../components/block-pattern-setup/style.scss | 30 +- .../components/block-styles/index.native.js | 3 +- .../src/components/block-styles/utils.js | 12 +- .../components/block-switcher/test/index.js | 5 +- .../components/block-toolbar/index.native.js | 19 +- .../block-tools/selected-block-popover.js | 4 +- .../block-variation-picker/index.js | 6 +- .../test/index.js | 2 + .../components/colors-gradients/control.js | 7 - .../components/colors-gradients/dropdown.js | 2 - .../panel-color-gradient-settings.js | 26 +- .../use-common-single-multiple-selects.js | 11 - ...se-multiple-origin-colors-and-gradients.js | 6 +- .../src/components/colors/test/with-colors.js | 2 + .../src/components/colors/utils.js | 8 +- .../src/components/copy-handler/index.js | 60 +- .../default-block-appender/test/index.js | 2 + .../src/components/font-sizes/fluid-utils.js | 8 +- .../src/components/font-sizes/utils.js | 8 +- .../components/font-sizes/with-font-sizes.js | 8 +- .../src/components/gradients/use-gradient.js | 9 +- .../src/components/iframe/index.js | 111 +- .../iframe/use-compatibility-styles.js | 95 + .../image-size-control/test/index.js | 226 +- .../src/components/inner-blocks/index.js | 6 +- .../inserter/hooks/use-insertion-point.js | 5 +- .../src/components/inserter/index.js | 17 +- .../components/inserter/media-tab/hooks.js | 9 +- .../src/components/inserter/quick-inserter.js | 3 + .../src/components/inserter/search-items.js | 3 +- .../src/components/inserter/search-results.js | 2 + .../src/components/link-control/test/index.js | 22 +- .../list-view/block-select-button.js | 2 +- .../src/components/list-view/style.scss | 8 +- .../media-replace-flow/test/index.js | 2 + .../components/off-canvas-editor/appender.js | 55 +- .../off-canvas-editor/block-select-button.js | 8 +- .../src/components/off-canvas-editor/block.js | 169 +- .../src/components/off-canvas-editor/index.js | 9 + .../components/off-canvas-editor/style.scss | 6 +- .../test/use-inserted-block.js | 108 + .../off-canvas-editor/use-inserted-block.js | 47 + .../responsive-block-control/label.js | 5 +- .../responsive-block-control/test/index.js | 6 +- .../rich-text/file-paste-handler.js | 13 - .../src/components/rich-text/format-edit.js | 16 +- .../src/components/rich-text/index.js | 1 + .../components/rich-text/use-paste-handler.js | 47 +- .../components/spacing-sizes-control/index.js | 1 - .../components/spacing-sizes-control/utils.js | 2 +- .../src/components/typewriter/index.js | 4 +- .../src/components/url-input/test/button.js | 2 + .../url-popover/image-url-input-ui.js | 9 +- .../src/components/url-popover/test/index.js | 26 +- .../src/components/warning/test/index.js | 2 + .../src/components/writing-flow/index.js | 2 +- packages/block-editor/src/hooks/border.js | 1 - .../block-editor/src/hooks/color-panel.js | 1 - packages/block-editor/src/hooks/color.js | 2 - .../block-editor/src/hooks/font-family.js | 8 +- packages/block-editor/src/hooks/font-size.js | 17 +- .../src/hooks/test/use-typography-props.js | 26 + .../src/hooks/use-typography-props.js | 22 +- packages/block-editor/src/store/reducer.js | 19 +- packages/block-editor/src/store/selectors.js | 5 +- packages/block-editor/src/utils/pasting.js | 12 +- packages/block-library/src/categories/edit.js | 2 +- packages/block-library/src/comments/index.php | 1 - .../src/cover/edit/inspector-controls.js | 8 +- packages/block-library/src/freeform/edit.js | 77 +- packages/block-library/src/freeform/modal.js | 111 + packages/block-library/src/gallery/edit.js | 19 +- packages/block-library/src/gallery/gallery.js | 16 +- packages/block-library/src/image/image.js | 1 + .../test/__snapshots__/edit.native.js.snap | 3 + .../src/latest-posts/test/edit.native.js | 49 + packages/block-library/src/media-text/edit.js | 1 + .../block-library/src/missing/edit.native.js | 19 +- .../test/__snapshots__/edit.native.js.snap | 7 + .../src/more/test/edit.native.js | 41 + .../block-library/src/navigation-link/edit.js | 63 +- .../src/navigation-link/index.php | 32 + .../src/navigation-submenu/edit.js | 129 +- .../src/navigation-submenu/index.php | 32 + .../src/navigation/edit/index.js | 7 +- .../edit/menu-inspector-controls.js | 99 +- .../edit/navigation-menu-selector.js | 28 +- .../src/navigation/edit/utils.js | 82 + .../block-library/src/navigation/editor.scss | 7 - .../block-library/src/navigation/index.php | 32 + .../block-library/src/navigation/style.scss | 1 - .../block-library/src/page-list-item/edit.js | 26 +- .../block-library/src/page-list/block.json | 19 +- packages/block-library/src/page-list/edit.js | 52 +- .../block-library/src/page-list/editor.scss | 4 + packages/block-library/src/page-list/index.js | 10 +- .../block-library/src/page-list/index.php | 19 +- .../src/post-featured-image/overlay.js | 1 - packages/block-library/src/query/constants.js | 7 - .../block-library/src/query/edit/index.js | 1 + .../query/edit/inspector-controls/index.js | 87 +- .../inspector-controls/taxonomy-controls.js | 140 +- .../src/query/edit/query-content.js | 3 +- .../src/query/edit/query-placeholder.js | 29 +- packages/block-library/src/query/utils.js | 60 +- packages/block-library/src/search/edit.js | 4 +- .../test/__snapshots__/edit.native.js.snap | 7 + .../src/separator/test/edit.native.js | 41 + .../block-library/src/social-link/edit.js | 6 +- .../block-library/src/social-links/edit.js | 1 - .../src/table-of-contents/edit.js | 2 +- packages/block-library/src/table/style.scss | 8 + packages/block-library/src/table/theme.scss | 8 - .../template-part/edit/advanced-controls.js | 12 + .../src/template-part/edit/import-controls.js | 180 + .../src/template-part/edit/index.js | 1 + .../template-part/edit/utils/transformers.js | 37 + packages/block-library/src/verse/style.scss | 5 +- .../test/__snapshots__/edit.native.js.snap | 13 + .../src/verse/test/edit.native.js | 73 +- packages/blocks/CHANGELOG.md | 2 +- packages/components/CHANGELOG.md | 8 + packages/components/CONTRIBUTING.md | 21 - packages/components/src/autocomplete/index.js | 4 +- .../components/src/autocomplete/test/index.js | 2 + .../src/base-control/test/index.tsx | 1 + .../component.tsx | 2 - .../border-box-control-split-controls/hook.ts | 2 - .../border-box-control/component.tsx | 7 - .../border-box-control/hook.ts | 2 - .../src/border-box-control/test/index.js | 2 + .../border-control-dropdown/component.tsx | 28 +- .../border-control-dropdown/hook.ts | 2 - .../border-control/component.tsx | 4 - .../src/border-control/border-control/hook.ts | 2 - .../src/border-control/stories/index.tsx | 1 - .../src/border-control/test/index.js | 137 +- .../components/src/border-control/types.ts | 5 +- .../components/src/box-control/test/index.js | 2 + .../src/checkbox-control/test/index.tsx | 2 + .../components/src/color-palette/index.tsx | 24 +- .../src/color-palette/stories/index.tsx | 13 - .../src/color-palette/test/index.tsx | 2 + .../components/src/color-palette/types.ts | 20 +- .../components/src/color-picker/test/index.js | 2 + .../src/combobox-control/test/index.js | 2 + .../src/confirm-dialog/test/index.js | 2 + .../src/date-time/date/test/index.tsx | 2 + .../src/date-time/time/test/index.tsx | 2 + .../components/src/dimension-control/index.js | 5 +- .../src/dimension-control/test/index.test.js | 2 + .../components/src/disabled/stories/index.tsx | 8 +- .../components/src/disabled/test/index.tsx | 2 + .../components/src/dropdown-menu/README.md | 5 +- .../src/dropdown-menu/test/index.js | 2 + packages/components/src/dropdown/README.md | 87 +- .../src/dropdown/{index.js => index.tsx} | 70 +- .../dropdown/stories/{index.js => index.tsx} | 29 +- .../src/dropdown/test/{index.js => index.tsx} | 18 +- packages/components/src/dropdown/types.ts | 107 + .../src/external-link/test/index.tsx | 2 + .../src/focal-point-picker/test/index.js | 2 + .../src/font-size-picker/test/index.tsx | 2 + .../src/form-file-upload/test/index.tsx | 2 + .../components/src/form-toggle/test/index.tsx | 2 + .../src/form-token-field/test/index.tsx | 3 + .../components/src/gradient-picker/index.js | 3 +- .../src/gradient-picker/stories/index.js | 1 - packages/components/src/guide/test/index.js | 2 + .../higher-order/navigate-regions/index.js | 7 +- .../higher-order/navigate-regions/style.scss | 52 +- .../higher-order/with-filters/test/index.js | 144 +- .../with-focus-outside/test/index.js | 2 + .../with-focus-return/test/index.js | 2 + .../higher-order/with-notices/test/index.js | 1 + packages/components/src/icon/stories/index.js | 128 - .../components/src/icon/stories/index.tsx | 103 + packages/components/src/index.js | 1 - .../src/input-control/test/index.js | 3 + .../isolated-event-container/test/index.js | 2 + .../mobile/bottom-sheet/picker-cell.native.js | 7 +- .../global-styles-context/utils.native.js | 33 +- packages/components/src/modal/test/index.tsx | 4 +- .../test/navigable-menu.js | 2 + .../test/tababble-container.js | 2 + .../components/src/navigation/test/index.js | 2 + .../components/src/navigator/test/index.tsx | 2 + packages/components/src/notice/test/index.js | 11 +- .../src/number-control/test/index.tsx | 2 + .../src/panel/test/__snapshots__/body.js.snap | 9 + packages/components/src/panel/test/body.js | 133 +- .../components/src/placeholder/test/index.tsx | 3 + .../select-control/test/select-control.tsx | 2 + packages/components/src/tab-panel/index.tsx | 3 +- .../components/src/tab-panel/test/index.tsx | 2 + .../src/text-highlight/test/index.tsx | 1 + .../src/toggle-group-control/test/index.tsx | 2 + .../src/toolbar-group/test/index.js | 2 + packages/components/src/toolbar/style.scss | 2 +- .../components/src/tooltip/stories/index.js | 146 +- packages/components/src/tooltip/test/index.js | 2 + .../src/unit-control/test/index.tsx | 3 + .../src/utils/hooks/test/use-latest-ref.js | 2 + .../src/higher-order/pure/test/index.js | 2 + .../higher-order/with-safe-timeout/index.tsx | 4 +- .../src/higher-order/with-state/test/index.js | 2 + .../src/hooks/use-focus-outside/test/index.js | 2 + .../core-data/src/queried-data/reducer.js | 29 +- .../src/queried-data/test/reducer.js | 40 + packages/customize-widgets/package.json | 3 +- packages/customize-widgets/src/utils.js | 2 +- .../src/components/use-select/test/index.js | 34 + .../components/use-select/test/suspense.js | 3 +- packages/data/src/registry.js | 11 +- packages/dom/src/tabbable.js | 37 +- .../src/site-editor/toggle-canvas-mode.js | 2 +- .../e2e-test-utils-playwright/src/test.ts | 2 +- packages/e2e-test-utils/README.md | 64 +- .../src/create-reusable-block.js | 3 +- packages/e2e-test-utils/src/index.js | 13 +- packages/e2e-test-utils/src/inserter.js | 352 +- packages/e2e-test-utils/src/site-editor.js | 4 +- .../assets/small-post-with-containers.html | 77 + .../e2e-tests/config/performance-reporter.js | 28 +- .../blocks/__snapshots__/quote.test.js.snap | 123 - .../specs/editor/blocks/quote.test.js | 199 - .../plugins/block-directory-add.test.js | 2 +- .../specs/editor/various/a11y.test.js | 4 +- .../block-hierarchy-navigation.test.js | 6 +- .../specs/editor/various/editor-modes.test.js | 10 +- .../editor/various/font-size-picker.test.js | 314 -- .../various/keyboard-navigable-blocks.test.js | 7 + .../specs/editor/various/sidebar.test.js | 2 - .../various/toolbar-roving-tabindex.test.js | 142 - .../specs/performance/post-editor.test.js | 142 +- .../specs/performance/site-editor.test.js | 1 + .../site-editor/multi-entity-saving.test.js | 25 +- packages/edit-post/package.json | 1 + .../test/listener-hooks.js | 1 + .../header/fullscreen-mode-close/style.scss | 2 +- .../options/test/enable-custom-fields.js | 2 + .../secondary-sidebar/list-view-outline.js | 24 +- .../components/secondary-sidebar/style.scss | 23 +- .../src/components/visual-editor/index.js | 29 +- packages/edit-post/src/index.js | 2 + packages/edit-site/CHANGELOG.md | 11 +- packages/edit-site/package.json | 1 + .../src/components/block-editor/index.js | 8 +- .../edit-site/src/components/editor/index.js | 17 +- .../global-styles/block-preview-panel.js | 33 +- .../components/global-styles/border-panel.js | 1 - .../components/global-styles/custom-css.js | 1 + .../global-styles/global-styles-provider.js | 16 +- .../global-styles/screen-background-color.js | 1 - .../components/global-styles/screen-block.js | 5 +- .../components/global-styles/screen-border.js | 2 + .../global-styles/screen-button-color.js | 2 - .../components/global-styles/screen-colors.js | 3 + .../global-styles/screen-heading-color.js | 4 +- .../components/global-styles/screen-layout.js | 2 + .../global-styles/screen-link-color.js | 1 - .../components/global-styles/screen-root.js | 61 +- .../global-styles/screen-text-color.js | 1 - .../global-styles/screen-typography.js | 3 + .../global-styles/test/typography-utils.js | 95 +- .../global-styles/typography-utils.js | 28 +- .../src/components/global-styles/utils.js | 11 +- .../document-actions/index.js | 55 +- .../components/header-edit-mode/style.scss | 1 + .../edit-site/src/components/layout/index.js | 287 +- .../src/components/layout/style.scss | 133 +- .../edit-site/src/components/list/style.scss | 9 +- .../src/components/sidebar-edit-mode/index.js | 2 +- .../navigation-menu.js | 10 + .../sidebar-navigation-screen-main/index.js | 30 +- .../index.js | 36 +- .../sidebar-navigation-screen/style.scss | 6 +- .../edit-site/src/components/sidebar/index.js | 5 +- .../src/components/site-hub/index.js | 150 + .../src/components/site-hub/style.scss | 31 + .../src/components/site-title/index.js | 39 - .../use-edited-entity-record/index.js | 37 + packages/edit-site/src/hooks/index.js | 1 + .../push-changes-to-global-styles/index.js | 162 + .../push-changes-to-global-styles/style.scss | 4 + packages/edit-site/src/index.js | 2 + packages/edit-site/src/style.scss | 2 + .../components/editor-help/index.native.js | 43 +- .../components/page-attributes/test/order.js | 2 + .../src/components/post-excerpt/index.js | 1 + .../post-preview-button/test/index.js | 2 + .../post-publish-button/test/index.js | 2 + .../components/post-saved-state/test/index.js | 2 + .../src/components/post-slug/test/index.js | 8 +- .../src/components/provider/index.native.js | 1 + packages/element/CHANGELOG.md | 4 + packages/element/README.md | 52 + packages/element/src/react-platform.js | 15 + packages/element/src/react.js | 48 +- packages/icons/src/index.js | 1 + packages/icons/src/library/lock-small.js | 16 + packages/icons/src/library/update.js | 4 +- .../components/interface-skeleton/index.js | 8 +- .../components/interface-skeleton/style.scss | 14 - .../src/components/navigable-region/index.js | 4 +- packages/jest-preset-default/jest-preset.js | 2 +- packages/keycodes/src/index.js | 6 +- packages/nux/.npmrc | 1 - packages/nux/CHANGELOG.md | 126 - packages/nux/README.md | 114 - packages/nux/package.json | 50 - packages/nux/src/components/dot-tip/README.md | 38 - packages/nux/src/components/dot-tip/index.js | 93 - .../nux/src/components/dot-tip/style.scss | 123 - .../dot-tip/test/__snapshots__/index.js.snap | 46 - .../nux/src/components/dot-tip/test/index.js | 78 - packages/nux/src/index.js | 13 - packages/nux/src/store/actions.js | 52 - packages/nux/src/store/index.js | 36 - packages/nux/src/store/reducer.js | 70 - packages/nux/src/store/selectors.js | 81 - packages/nux/src/store/test/actions.js | 40 - packages/nux/src/store/test/reducer.js | 69 - packages/nux/src/store/test/selectors.js | 146 - packages/nux/src/style.scss | 1 - packages/react-native-aztec/package.json | 2 +- .../mobile/WPAndroidGlue/GutenbergProps.kt | 6 + .../local-storage-overrides.json | 6 - .../ios/GutenbergBridgeDelegate.swift | 2 + packages/react-native-bridge/package.json | 2 +- packages/react-native-editor/CHANGELOG.md | 7 +- ...gutenberg-editor-block-insertion-2.test.js | 38 + .../gutenberg-editor-block-insertion.test.js | 12 +- .../gutenberg-editor-columns.test.js | 19 - .../gutenberg-editor-latest-posts.test.js | 16 - .../gutenberg-editor-lists-@canary.test.js | 61 - .../gutenberg-editor-lists-end.test.js | 27 - .../gutenberg-editor-lists.test.js | 47 - .../gutenberg-editor-more.test.js | 16 - .../gutenberg-editor-separator.test.js | 16 - .../gutenberg-editor-spacer.test.js | 16 - .../gutenberg-editor-verse.test.js | 16 - .../__device-tests__/helpers/caps.js | 1 + .../__device-tests__/helpers/utils.js | 12 +- .../__device-tests__/pages/editor-page.js | 14 +- .../GutenbergViewController.swift | 1 + packages/react-native-editor/ios/Podfile.lock | 14 +- packages/react-native-editor/package.json | 4 +- packages/report-flaky-tests/action.yml | 6 +- packages/report-flaky-tests/package.json | 3 +- .../__tests__/__snapshots__/run.test.ts.snap | 191 +- .../src/__tests__/markdown.test.ts | 52 + .../src/__tests__/run.test.ts | 222 +- packages/report-flaky-tests/src/github-api.ts | 72 +- packages/report-flaky-tests/src/markdown.ts | 28 +- packages/report-flaky-tests/src/run.ts | 70 +- packages/report-flaky-tests/src/types.ts | 7 + .../rich-text/src/register-format-type.js | 5 +- .../src/test/register-format-type.js | 13 +- packages/scripts/config/puppeteer.config.js | 5 +- packages/server-side-render/package.json | 3 +- .../style-engine/class-wp-style-engine.php | 6 + .../src/styles/typography/index.ts | 13 + packages/style-engine/src/test/index.js | 9 +- packages/style-engine/src/types.ts | 1 + packages/widgets/src/index.js | 14 +- phpunit/block-supports/typography-test.php | 49 +- phpunit/block-template-utils-test.php | 135 +- ...ass-block-library-navigation-link-test.php | 14 +- ...rg-rest-block-patterns-controller-test.php | 31 +- phpunit/class-wp-theme-json-test.php | 90 +- phpunit/class-wp-webfonts-test.php | 453 -- .../style.css | 8 + .../theme.json | 11 + phpunit/fixtures/mock-provider.php | 27 + phpunit/html/wp-html-tag-processor-test.php | 112 +- phpunit/multisite.xml | 1 + phpunit/style-engine/style-engine-test.php | 4 +- phpunit/webfonts/wp-webfonts-testcase.php | 268 ++ .../webfonts/wp-webfonts-tests-dataset.php | 1155 +++++ .../webfonts/wpDeregisterFontFamily-test.php | 74 + .../wpDeregisterWebfontVariation-test.php | 298 ++ .../wpEnqueueWebfontVariations-test.php | 82 + phpunit/webfonts/wpEnqueueWebfonts-test.php | 122 + phpunit/webfonts/wpPrintWebfonts-test.php | 127 + .../wpRegisterWebfontProvider-test.php | 96 + phpunit/webfonts/wpRegisterWebfonts-test.php | 325 ++ phpunit/webfonts/wpWebfonts-test.php | 38 + phpunit/webfonts/wpWebfonts/add-test.php | 45 + .../wpWebfonts/addFontFamily-test.php | 66 + .../webfonts/wpWebfonts/addVariation-test.php | 149 + phpunit/webfonts/wpWebfonts/dequeue-test.php | 72 + phpunit/webfonts/wpWebfonts/doItem-test.php | 336 ++ phpunit/webfonts/wpWebfonts/doItems-test.php | 196 + phpunit/webfonts/wpWebfonts/enqueue-test.php | 53 + .../webfonts/wpWebfonts/getEnqueued-test.php | 57 + .../webfonts/wpWebfonts/getProviders-test.php | 62 + .../wpWebfonts/getRegistered-test.php | 90 + phpunit/webfonts/wpWebfonts/query-test.php | 151 + .../wpWebfonts/registerProvider-test.php | 116 + phpunit/webfonts/wpWebfonts/remove-test.php | 118 + .../wpWebfonts/removeFontFamily-test.php | 89 + .../wpWebfonts/removeVariation-test.php | 278 ++ .../wpWebfontsProviderLocal-test.php} | 91 +- .../convertFontFamilyIntoHandle-test.php | 84 + .../convertVariationIntoHandle-test.php | 122 + .../getFontFamilyFromVariation-test.php | 134 + .../wpWebfontsUtils/isDefined-test.php | 61 + schemas/json/block.json | 4 +- schemas/json/theme.json | 473 +- storybook/webpack/source-link-loader.js | 3 +- test/e2e/specs/editor/blocks/comments.spec.js | 31 +- .../e2e/specs/editor/blocks/paragraph.spec.js | 8 +- test/e2e/specs/editor/blocks/quote.spec.js | 312 +- .../various/a11y-region-navigation.spec.js | 5 +- .../editor/various/font-size-picker.spec.js | 305 ++ .../various/toolbar-roving-tabindex.spec.js | 216 + .../site-editor/push-to-global-styles.spec.js | 109 + .../fixtures/blocks/core__page-list.json | 3 +- .../@wordpress/react-native-aztec/index.js | 6 +- test/native/__mocks__/styleMock.js | 1 + .../initialize-editor.js | 5 +- 511 files changed, 19514 insertions(+), 8872 deletions(-) delete mode 100644 .github/workflows/end2end-test-playwright.yml delete mode 100644 .github/workflows/flaky-tests.yml delete mode 100644 docs/reference-guides/data/data-core-nux.md rename lib/{compat/wordpress-6.1/class-wp-theme-json-6-1.php => class-wp-theme-json-gutenberg.php} (52%) create mode 100644 lib/class-wp-theme-json-resolver-gutenberg.php delete mode 100644 lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php create mode 100644 lib/compat/wordpress-6.2/block-template-utils.php delete mode 100644 lib/compat/wordpress-6.2/class-wp-theme-json-6-2.php delete mode 100644 lib/compat/wordpress-6.2/class-wp-theme-json-resolver-6-2.php create mode 100644 lib/compat/wordpress-6.2/theme.php create mode 100644 lib/compat/wordpress-6.2/widgets.php delete mode 100644 lib/experimental/class-wp-theme-json-gutenberg.php delete mode 100644 lib/experimental/class-wp-theme-json-resolver-gutenberg.php create mode 100644 lib/experimental/class-wp-web-fonts.php create mode 100644 lib/experimental/class-wp-webfonts-utils.php create mode 100644 lib/experimental/webfonts-deprecations.php rename lib/{compat/wordpress-6.1 => }/theme-i18n.json (100%) rename lib/{compat/wordpress-6.1 => }/theme.json (96%) create mode 100644 packages/base-styles/_long-content-fade.scss create mode 100644 packages/block-editor/src/components/block-mobile-toolbar/test/__snapshots__/block-actions-menu.native.js.snap create mode 100644 packages/block-editor/src/components/block-mobile-toolbar/test/block-actions-menu.native.js delete mode 100644 packages/block-editor/src/components/colors-gradients/use-common-single-multiple-selects.js create mode 100644 packages/block-editor/src/components/iframe/use-compatibility-styles.js create mode 100644 packages/block-editor/src/components/off-canvas-editor/test/use-inserted-block.js create mode 100644 packages/block-editor/src/components/off-canvas-editor/use-inserted-block.js delete mode 100644 packages/block-editor/src/components/rich-text/file-paste-handler.js create mode 100644 packages/block-library/src/freeform/modal.js create mode 100644 packages/block-library/src/latest-posts/test/__snapshots__/edit.native.js.snap create mode 100644 packages/block-library/src/latest-posts/test/edit.native.js create mode 100644 packages/block-library/src/more/test/__snapshots__/edit.native.js.snap create mode 100644 packages/block-library/src/more/test/edit.native.js delete mode 100644 packages/block-library/src/query/constants.js create mode 100644 packages/block-library/src/separator/test/__snapshots__/edit.native.js.snap create mode 100644 packages/block-library/src/separator/test/edit.native.js create mode 100644 packages/block-library/src/template-part/edit/import-controls.js create mode 100644 packages/block-library/src/template-part/edit/utils/transformers.js create mode 100644 packages/block-library/src/verse/test/__snapshots__/edit.native.js.snap rename packages/components/src/dropdown/{index.js => index.tsx} (66%) rename packages/components/src/dropdown/stories/{index.js => index.tsx} (75%) rename packages/components/src/dropdown/test/{index.js => index.tsx} (77%) delete mode 100644 packages/components/src/icon/stories/index.js create mode 100644 packages/components/src/icon/stories/index.tsx create mode 100644 packages/components/src/panel/test/__snapshots__/body.js.snap create mode 100644 packages/e2e-tests/assets/small-post-with-containers.html delete mode 100644 packages/e2e-tests/specs/editor/blocks/__snapshots__/quote.test.js.snap delete mode 100644 packages/e2e-tests/specs/editor/blocks/quote.test.js delete mode 100644 packages/e2e-tests/specs/editor/various/font-size-picker.test.js delete mode 100644 packages/e2e-tests/specs/editor/various/toolbar-roving-tabindex.test.js create mode 100644 packages/edit-site/src/components/site-hub/index.js create mode 100644 packages/edit-site/src/components/site-hub/style.scss delete mode 100644 packages/edit-site/src/components/site-title/index.js create mode 100644 packages/edit-site/src/components/use-edited-entity-record/index.js create mode 100644 packages/edit-site/src/hooks/push-changes-to-global-styles/index.js create mode 100644 packages/edit-site/src/hooks/push-changes-to-global-styles/style.scss create mode 100644 packages/icons/src/library/lock-small.js delete mode 100644 packages/nux/.npmrc delete mode 100644 packages/nux/CHANGELOG.md delete mode 100644 packages/nux/README.md delete mode 100644 packages/nux/package.json delete mode 100644 packages/nux/src/components/dot-tip/README.md delete mode 100644 packages/nux/src/components/dot-tip/index.js delete mode 100644 packages/nux/src/components/dot-tip/style.scss delete mode 100644 packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap delete mode 100644 packages/nux/src/components/dot-tip/test/index.js delete mode 100644 packages/nux/src/index.js delete mode 100644 packages/nux/src/store/actions.js delete mode 100644 packages/nux/src/store/index.js delete mode 100644 packages/nux/src/store/reducer.js delete mode 100644 packages/nux/src/store/selectors.js delete mode 100644 packages/nux/src/store/test/actions.js delete mode 100644 packages/nux/src/store/test/reducer.js delete mode 100644 packages/nux/src/store/test/selectors.js delete mode 100644 packages/nux/src/style.scss delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-columns.test.js delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-latest-posts.test.js delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-lists-@canary.test.js delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-lists-end.test.js delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-lists.test.js delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-more.test.js delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-separator.test.js delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-spacer.test.js delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-verse.test.js delete mode 100644 phpunit/class-wp-webfonts-test.php create mode 100644 phpunit/data/themedir1/block-theme-child-with-fluid-typography-config/style.css create mode 100644 phpunit/data/themedir1/block-theme-child-with-fluid-typography-config/theme.json create mode 100644 phpunit/fixtures/mock-provider.php create mode 100644 phpunit/webfonts/wp-webfonts-testcase.php create mode 100644 phpunit/webfonts/wp-webfonts-tests-dataset.php create mode 100644 phpunit/webfonts/wpDeregisterFontFamily-test.php create mode 100644 phpunit/webfonts/wpDeregisterWebfontVariation-test.php create mode 100644 phpunit/webfonts/wpEnqueueWebfontVariations-test.php create mode 100644 phpunit/webfonts/wpEnqueueWebfonts-test.php create mode 100644 phpunit/webfonts/wpPrintWebfonts-test.php create mode 100644 phpunit/webfonts/wpRegisterWebfontProvider-test.php create mode 100644 phpunit/webfonts/wpRegisterWebfonts-test.php create mode 100644 phpunit/webfonts/wpWebfonts-test.php create mode 100644 phpunit/webfonts/wpWebfonts/add-test.php create mode 100644 phpunit/webfonts/wpWebfonts/addFontFamily-test.php create mode 100644 phpunit/webfonts/wpWebfonts/addVariation-test.php create mode 100644 phpunit/webfonts/wpWebfonts/dequeue-test.php create mode 100644 phpunit/webfonts/wpWebfonts/doItem-test.php create mode 100644 phpunit/webfonts/wpWebfonts/doItems-test.php create mode 100644 phpunit/webfonts/wpWebfonts/enqueue-test.php create mode 100644 phpunit/webfonts/wpWebfonts/getEnqueued-test.php create mode 100644 phpunit/webfonts/wpWebfonts/getProviders-test.php create mode 100644 phpunit/webfonts/wpWebfonts/getRegistered-test.php create mode 100644 phpunit/webfonts/wpWebfonts/query-test.php create mode 100644 phpunit/webfonts/wpWebfonts/registerProvider-test.php create mode 100644 phpunit/webfonts/wpWebfonts/remove-test.php create mode 100644 phpunit/webfonts/wpWebfonts/removeFontFamily-test.php create mode 100644 phpunit/webfonts/wpWebfonts/removeVariation-test.php rename phpunit/{class-wp-webfonts-local-provider-test.php => webfonts/wpWebfontsProviderLocal-test.php} (78%) create mode 100644 phpunit/webfonts/wpWebfontsUtils/convertFontFamilyIntoHandle-test.php create mode 100644 phpunit/webfonts/wpWebfontsUtils/convertVariationIntoHandle-test.php create mode 100644 phpunit/webfonts/wpWebfontsUtils/getFontFamilyFromVariation-test.php create mode 100644 phpunit/webfonts/wpWebfontsUtils/isDefined-test.php create mode 100644 test/e2e/specs/editor/various/font-size-picker.spec.js create mode 100644 test/e2e/specs/editor/various/toolbar-roving-tabindex.spec.js create mode 100644 test/e2e/specs/site-editor/push-to-global-styles.spec.js diff --git a/.eslintrc.js b/.eslintrc.js index 51bca0f3bc059b..46c930ee0fb732 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -68,6 +68,7 @@ const restrictedImports = [ 'every', 'extend', 'filter', + 'find', 'findIndex', 'findKey', 'findLast', @@ -106,6 +107,7 @@ const restrictedImports = [ 'negate', 'noop', 'nth', + 'omit', 'omitBy', 'once', 'orderby', @@ -370,9 +372,6 @@ module.exports = { 'plugin:jest-dom/recommended', 'plugin:testing-library/react', ], - rules: { - 'testing-library/no-node-access': 'off', - }, }, { files: [ 'packages/e2e-test*/**/*.js' ], diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2cc7b0ce34bd1e..6427516035a5ec 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -87,7 +87,6 @@ /packages/compose @ajitbohra /packages/element @ajitbohra /packages/notices @ajitbohra -/packages/nux @ajitbohra /packages/viewport @ajitbohra /packages/base-styles /packages/icons diff --git a/.github/workflows/end2end-test-playwright.yml b/.github/workflows/end2end-test-playwright.yml deleted file mode 100644 index e0aabb17afa019..00000000000000 --- a/.github/workflows/end2end-test-playwright.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: End-to-End Tests Playwright - -on: - pull_request: - push: - branches: - - trunk - - 'release/**' - - 'wp/**' - -# Cancels all previous workflow runs for pull requests that have not completed. -concurrency: - # The concurrency group contains the workflow name and the branch name for pull requests - # or the commit hash for any other events. - group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} - cancel-in-progress: true - -jobs: - e2e: - name: E2E Tests - runs-on: ubuntu-latest - if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} - strategy: - fail-fast: false - - steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 - - - name: Use desired version of NodeJS - uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # v3.5.1 - with: - node-version-file: '.nvmrc' - cache: npm - - - name: Npm install and build - run: | - npm ci - npm run build - - - name: Install Playwright dependencies - run: | - npx playwright install chromium firefox webkit --with-deps - - - name: Install WordPress and start the server - run: | - npm run wp-env start - - - name: Run the tests - run: | - xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test:e2e:playwright - - - name: Archive debug artifacts (screenshots, traces) - uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # v3.1.1 - if: always() - with: - name: failures-artifacts - path: artifacts/test-results - if-no-files-found: ignore - - - name: Archive flaky tests report - uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # v3.1.1 - if: always() - with: - name: flaky-tests-report-playwright - path: flaky-tests - if-no-files-found: ignore diff --git a/.github/workflows/end2end-test.yml b/.github/workflows/end2end-test.yml index 172d3b090eaf0b..46278d6384a5b4 100644 --- a/.github/workflows/end2end-test.yml +++ b/.github/workflows/end2end-test.yml @@ -16,8 +16,8 @@ concurrency: cancel-in-progress: true jobs: - admin: - name: Admin - ${{ matrix.part }} + e2e-puppeteer: + name: Puppeteer - ${{ matrix.part }} runs-on: ubuntu-latest if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} strategy: @@ -60,6 +60,97 @@ jobs: uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # v3.1.1 if: always() with: - name: flaky-tests-report-${{ matrix.part }} + name: flaky-tests-report path: flaky-tests if-no-files-found: ignore + + e2e-playwright: + name: Playwright + runs-on: ubuntu-latest + if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} + strategy: + fail-fast: false + + steps: + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 + + - name: Use desired version of NodeJS + uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # v3.5.1 + with: + node-version-file: '.nvmrc' + cache: npm + + - name: Npm install and build + run: | + npm ci + npm run build + + - name: Install Playwright dependencies + run: | + npx playwright install chromium firefox webkit --with-deps + + - name: Install WordPress and start the server + run: | + npm run wp-env start + + - name: Run the tests + run: | + xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test:e2e:playwright + + - name: Archive debug artifacts (screenshots, traces) + uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # v3.1.1 + if: always() + with: + name: failures-artifacts + path: artifacts/test-results + if-no-files-found: ignore + + - name: Archive flaky tests report + uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # v3.1.1 + if: always() + with: + name: flaky-tests-report + path: flaky-tests + if-no-files-found: ignore + + report-to-issues: + name: Report to GitHub + needs: [e2e-puppeteer, e2e-playwright] + if: ${{ always() }} + runs-on: ubuntu-latest + steps: + # Checkout defaults to using the branch which triggered the event, which + # isn't necessarily `trunk` (e.g. in the case of a merge). + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 + with: + ref: trunk + + - uses: actions/download-artifact@v3 + id: download_artifact + # Don't fail the job if there isn't any flaky tests report. + continue-on-error: true + with: + name: flaky-tests-report + path: flaky-tests + + - name: Use desired version of NodeJS + if: ${{ steps.download_artifact.outcome == 'success' }} + uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # v3.5.1 + with: + node-version-file: '.nvmrc' + cache: npm + + - name: Npm install and build + if: ${{ steps.download_artifact.outcome == 'success' }} + # TODO: We don't have to build the entire project, just the action itself. + run: | + npm ci + npm run build:packages + + - name: Report flaky tests + if: ${{ steps.download_artifact.outcome == 'success' }} + uses: ./packages/report-flaky-tests + with: + repo-token: '${{ secrets.GITHUB_TOKEN }}' + label: '[Type] Flaky Test' + artifact-path: flaky-tests diff --git a/.github/workflows/flaky-tests.yml b/.github/workflows/flaky-tests.yml deleted file mode 100644 index 6f457f6b2292c5..00000000000000 --- a/.github/workflows/flaky-tests.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Report Flaky Tests - -on: - workflow_run: - workflows: ['End-to-End Tests', 'End-to-End Tests Playwright'] - types: - - completed - -jobs: - report-to-issues: - name: Report to GitHub issues - runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'success' }} - steps: - # Checkout defaults to using the branch which triggered the event, which - # isn't necessarily `trunk` (e.g. in the case of a merge). - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 - with: - ref: trunk - - - name: Use desired version of NodeJS - uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # v3.5.1 - with: - node-version-file: '.nvmrc' - cache: npm - - - name: Npm install and build - # TODO: We don't have to build the entire project, just the action itself. - run: | - npm ci - npm run build:packages - - - name: Report flaky tests - uses: ./packages/report-flaky-tests - with: - repo-token: '${{ secrets.GITHUB_TOKEN }}' - label: '[Type] Flaky Test' - artifact-name-prefix: flaky-tests-report diff --git a/bin/api-docs/gen-theme-reference.js b/bin/api-docs/gen-theme-reference.js index d051261963daf4..45edecf6bd1a2f 100644 --- a/bin/api-docs/gen-theme-reference.js +++ b/bin/api-docs/gen-theme-reference.js @@ -127,7 +127,8 @@ const getStylePropertiesMarkup = ( struct ) => { props[ key ].type === 'object' ? keys( props[ key ].properties ).sort().join( ', ' ) : ''; - markup += `| ${ key } | ${ props[ key ].type } | ${ ps } |\n`; + const type = formatType( props[ key ] ); + markup += `| ${ key } | ${ type } | ${ ps } |\n`; } ); return markup; @@ -160,6 +161,32 @@ ${ markupFn( data ) } let autogen = ''; +/** + * Format list of types. + * + * @param {Object} prop + * @return {string} type + */ +const formatType = ( prop ) => { + let type = prop.type || ''; + + if ( prop.hasOwnProperty( 'anyOf' ) || prop.hasOwnProperty( 'oneOf' ) ) { + const propTypes = prop.anyOf || prop.oneOf; + const types = []; + + propTypes.forEach( ( item ) => { + if ( item.type ) types.push( item.type ); + // refComplete is always an object + if ( item.$ref && item.$ref === '#/definitions/refComplete' ) + types.push( 'object' ); + } ); + + type = [ ...new Set( types ) ].join( ', ' ); + } + + return type; +}; + // Settings const settings = Object.entries( themejson.definitions ) .filter( ( [ settingsKey ] ) => diff --git a/bin/plugin/commands/performance.js b/bin/plugin/commands/performance.js index 31a537f373731d..81aa39820c8b5a 100644 --- a/bin/plugin/commands/performance.js +++ b/bin/plugin/commands/performance.js @@ -37,6 +37,7 @@ const config = require( '../config' ); * @property {number[]} firstContentfulPaint Represents the time when the browser first renders any text or media. * @property {number[]} firstBlock Represents the time when Puppeteer first sees a block selector in the DOM. * @property {number[]} type Average type time. + * @property {number[]} typeContainer Average type time within a container. * @property {number[]} focus Average block selection time. * @property {number[]} inserterOpen Average time to open global inserter. * @property {number[]} inserterSearch Average time to search the inserter. @@ -56,6 +57,9 @@ const config = require( '../config' ); * @property {number=} type Average type time. * @property {number=} minType Minimum type time. * @property {number=} maxType Maximum type time. + * @property {number=} typeContainer Average type time within a container. + * @property {number=} minTypeContainer Minimum type time within a container. + * @property {number=} maxTypeContainer Maximum type time within a container. * @property {number=} focus Average block selection time. * @property {number=} minFocus Min block selection time. * @property {number=} maxFocus Max block selection time. @@ -129,6 +133,9 @@ function curateResults( results ) { type: average( results.type ), minType: Math.min( ...results.type ), maxType: Math.max( ...results.type ), + typeContainer: average( results.typeContainer ), + minTypeContainer: Math.min( ...results.typeContainer ), + maxTypeContainer: Math.max( ...results.typeContainer ), focus: average( results.focus ), minFocus: Math.min( ...results.focus ), maxFocus: Math.max( ...results.focus ), @@ -393,6 +400,15 @@ async function runPerformanceTests( branches, options ) { type: rawResults.map( ( r ) => r[ branch ].type ), minType: rawResults.map( ( r ) => r[ branch ].minType ), maxType: rawResults.map( ( r ) => r[ branch ].maxType ), + typeContainer: rawResults.map( + ( r ) => r[ branch ].typeContainer + ), + minTypeContainer: rawResults.map( + ( r ) => r[ branch ].minTypeContainer + ), + maxTypeContainer: rawResults.map( + ( r ) => r[ branch ].maxTypeContainer + ), focus: rawResults.map( ( r ) => r[ branch ].focus ), minFocus: rawResults.map( ( r ) => r[ branch ].minFocus ), maxFocus: rawResults.map( ( r ) => r[ branch ].maxFocus ), diff --git a/changelog.txt b/changelog.txt index 2d52f25c1bf8a6..93cdbecad9ad8d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,648 @@ == Changelog == += 14.8.4 = + + + +## Changelog + +### Various + +- Fixes an issue where gutenberg bundled `theme.json` was not utilized. ([46810](https://github.com/WordPress/gutenberg/pull/46810)) This meant that Gutenberg features newer than the WordPress `theme.json` file were unavailable in 14.8. + +## Contributors + +The following contributors merged PRs in this release: + +@madhusudhand + + += 14.8.3 = + + + +## Changelog + +### Bug Fixes + +#### Plugin +Fixes compatibility with WordPress 6.0.x. This includes the following PRs: +- #46579 +- #46750 +- #46806 +- #46809 + +Broadly, this needed to include the two refactors of the Theme_JSON compatibility files, along with two fixes switching `wp_*` and `gutenberg_*` function variants as needed. + +## Contributors + +The following contributors were involved with this release: + +@geriux @Mamaduka @oandregal @noahtallen + + += 14.8.2 = + + + +## Changelog + +### Bug Fixes + +#### Block Editor +- rich-text: Fix bug where bare tag name format types could not be registered. ([46798](https://github.com/WordPress/gutenberg/pull/46798)) + + +## First time contributors + +The following PRs were merged by first time contributors: + + + +## Contributors + +The following contributors merged PRs in this release: + +@davilera + + += 14.8.1 = + +## Changelog + +### Bug Fixes + +- Update the script registration of the react scripts with a cache version to prevent browser cache issues. ([46768](https://github.com/WordPress/gutenberg/pull/46768)) + + +## Contributors + +The following contributors merged PRs in this release: + +@youknowriad + + += 14.8.0 = + +## Changelog + +### Enhancements + +#### Block Library +- Add a current-menu-ancestor class to navigation items. ([40778](https://github.com/WordPress/gutenberg/pull/40778)) +- Page List Block: Adds a longdash tree to the parent selector. ([46336](https://github.com/WordPress/gutenberg/pull/46336)) +- Page List Block: Hide page list edit button if no pages are available. ([46331](https://github.com/WordPress/gutenberg/pull/46331)) +- Page List: Add convert panel to Inspector Controls when within Nav block. ([46352](https://github.com/WordPress/gutenberg/pull/46352)) +- Page List: Prevent users from adding inner blocks to Page List. ([46269](https://github.com/WordPress/gutenberg/pull/46269)) +- Reusable block: Pluralize the message "Convert to regular blocks" depending on the number of blocks contained. ([45819](https://github.com/WordPress/gutenberg/pull/45819)) +- Add page list to Link UI transforms in Nav block. ([46426](https://github.com/WordPress/gutenberg/pull/46426)) +- Heading Block: Don't rely on the experimental selector anymore. ([46284](https://github.com/WordPress/gutenberg/pull/46284)) +- Media & Text Block: Create undo history when media width is changed. ([46084](https://github.com/WordPress/gutenberg/pull/46084)) +- Navigation block: Add location->primary to fallback nav creation for classic menus. ([45976](https://github.com/WordPress/gutenberg/pull/45976)) +- Navigation block: Update fallback nav creation to the most recently created menu. ([46286](https://github.com/WordPress/gutenberg/pull/46286)) +- Navigation: Add a 'open list view' button. ([46335](https://github.com/WordPress/gutenberg/pull/46335)) +- Navigation: Removes the header from the navigation list view in the experiment. ([46070](https://github.com/WordPress/gutenberg/pull/46070)) +- Query: Remove color block supports. ([46147](https://github.com/WordPress/gutenberg/pull/46147)) +- Table block: Make `figcaption` styles consistent between editor and front end. ([46172](https://github.com/WordPress/gutenberg/pull/46172)) +- List/quote: Unwrap inner block when pressing Backspace at start. ([45075](https://github.com/WordPress/gutenberg/pull/45075)) + +#### Inspector Controls +- Sidebar Tabs: Refine the use of inspector tabs and disable filters for Nav blocks. ([46346](https://github.com/WordPress/gutenberg/pull/46346)) +- Sidebar Tabs: Use editor settings to override display. ([46321](https://github.com/WordPress/gutenberg/pull/46321)) +- Summary panel: Try improving spacing and grid. ([46267](https://github.com/WordPress/gutenberg/pull/46267)) + +#### Global Styles +- Add Style Book to Global Styles. ([45960](https://github.com/WordPress/gutenberg/pull/45960)) +- Add block preview component in global styles. ([45719](https://github.com/WordPress/gutenberg/pull/45719)) +- Move border from layout to own menu. ([45995](https://github.com/WordPress/gutenberg/pull/45995)) +- Add a css style to theme.json to allow setting of custom css strings. ([46255](https://github.com/WordPress/gutenberg/pull/46255)) +- Expose before filter hook in useSettings for injecting block settings in the editor. ([45089](https://github.com/WordPress/gutenberg/pull/45089)) +- Global styles: Add custom CSS panel to site editor. ([46141](https://github.com/WordPress/gutenberg/pull/46141)) + +#### Site Editor +- Reorganize the site editor to introduce Browse Mode. ([44770](https://github.com/WordPress/gutenberg/pull/44770)) +- Allow adding new templates and template parts directly from the sidebar. ([46458](https://github.com/WordPress/gutenberg/pull/46458)) +- Synchronize the sidebar state in the URL. ([46433](https://github.com/WordPress/gutenberg/pull/46433)) +- Try template drill down on the shell sidebar (browse mode). ([45100](https://github.com/WordPress/gutenberg/pull/45100)) + +#### Block Editor +- Update the synced block hover styles in Inserter. ([46442](https://github.com/WordPress/gutenberg/pull/46442)) +- Add new selector getLastInsertedBlockClientId. ([46531](https://github.com/WordPress/gutenberg/pull/46531)) +- Block editor: Hide fixed contextual toolbar. ([46298](https://github.com/WordPress/gutenberg/pull/46298)) +- Inserter: Pattern title tooltip. ([46419](https://github.com/WordPress/gutenberg/pull/46419)) +- useNestedSettingsUpdate: Prevent unneeded syncing of falsy templateLock values. ([46357](https://github.com/WordPress/gutenberg/pull/46357)) +- Design: Augmented shadows for modals and popovers. ([46228](https://github.com/WordPress/gutenberg/pull/46228)) + +#### Components +- Tabs: Try a simpler tab focus style, alt. ([46276](https://github.com/WordPress/gutenberg/pull/46276)) +- BaseControl: Add convenience hook to generate id-related props. ([46170](https://github.com/WordPress/gutenberg/pull/46170)) +- Dashicon: Refactor to TypeScript. ([45924](https://github.com/WordPress/gutenberg/pull/45924)) +- Lighten borders to gray-600. ([46252](https://github.com/WordPress/gutenberg/pull/46252)) +- Popover: Check positioning by adding and testing is-positioned class. ([46429](https://github.com/WordPress/gutenberg/pull/46429)) + +### Icons +- Icons: Update the border icon. ([46264](https://github.com/WordPress/gutenberg/pull/46264)) + +#### Testing +- Tests: Fix `toBePositionedPopover` matcher message function. ([46239](https://github.com/WordPress/gutenberg/pull/46239)) + +#### Plugin +- Update the Gutenberg plugin to require at least the WP 6.0 version. ([46102](https://github.com/WordPress/gutenberg/pull/46102)) +- PHP: Backport changes from core theme resolver. ([46250](https://github.com/WordPress/gutenberg/pull/46250)) +- Update: Move gutenberg_register_core_block_patterns from 6.1 to 6.2. ([46249](https://github.com/WordPress/gutenberg/pull/46249)) +- Upgrade React packages to v18. ([45235](https://github.com/WordPress/gutenberg/pull/45235)) + +#### Themes +- Empty Theme: Add the `$schema` property in `theme.json` and rename template directories. ([46300](https://github.com/WordPress/gutenberg/pull/46300)) + +#### Mobile +- Mobile: Disable Unsupported Block Editor Tests (Android). ([46542](https://github.com/WordPress/gutenberg/pull/46542)) +- Mobile: Inserter - Remove `.done()` usage. ([46460](https://github.com/WordPress/gutenberg/pull/46460)) +- Mobile: Update Heading block end-to-end test. ([46220](https://github.com/WordPress/gutenberg/pull/46220)) +- Mobile: Updates packages to not use Git HTTPS URLs. ([46422](https://github.com/WordPress/gutenberg/pull/46422)) + +### Bug Fixes + +#### Block Library +- Fix Nav Submenu block Link UI text control. ([46243](https://github.com/WordPress/gutenberg/pull/46243)) +- Fix auto Nav menu creation due to page list inner blocks. ([46223](https://github.com/WordPress/gutenberg/pull/46223)) +- Handle innerContent too when removing innerBlocks. ([46377](https://github.com/WordPress/gutenberg/pull/46377)) +- Image Block: Ensure drag handle matches cursor position when resizing a center aligned image. ([46497](https://github.com/WordPress/gutenberg/pull/46497)) +- Navigation Block: Add social link singular to list of blocks to be allowed. ([46374](https://github.com/WordPress/gutenberg/pull/46374)) +- Navigation Block: Fixes adding a submenu. ([46364](https://github.com/WordPress/gutenberg/pull/46364)) +- Navigation Block: Prevent circular references in navigation block rendering. ([46387](https://github.com/WordPress/gutenberg/pull/46387)) +- Navigation Block: Recursively remove Navigation block’s from appearing inside Navigation block on front of site. ([46279](https://github.com/WordPress/gutenberg/pull/46279)) +- Navigation link: Use stripHTML. ([46317](https://github.com/WordPress/gutenberg/pull/46317)) +- Page List Block: Fix error loading page list parent options. ([46327](https://github.com/WordPress/gutenberg/pull/46327)) +- Query Loop Block: Add migration of colors to v2 deprecation. ([46522](https://github.com/WordPress/gutenberg/pull/46522)) +- Site Logo: Correctly set the image's natural height and width. ([46214](https://github.com/WordPress/gutenberg/pull/46214)) +- Strip markup from link label data in inspector. ([46171](https://github.com/WordPress/gutenberg/pull/46171)) +- Template Parts: Fix modal search stacking context. ([46421](https://github.com/WordPress/gutenberg/pull/46421)) +- Video: Avoid an error when removal is locked. ([46324](https://github.com/WordPress/gutenberg/pull/46324)) +- Layout child fixed size should not be fixed by default and should always have a value set. ([46139](https://github.com/WordPress/gutenberg/pull/46139)) + +#### Blocks +- Paste handler: Remove styles on inline paste. ([46402](https://github.com/WordPress/gutenberg/pull/46402)) +- Improve performance of gutenberg_render_layout_support_flag. ([46074](https://github.com/WordPress/gutenberg/pull/46074)) + +#### Global Styles +- Allow indirect properties when unfiltered_html is not allowed. ([46388](https://github.com/WordPress/gutenberg/pull/46388)) +- Fix Reset to defaults action by moving fills to be within context provider. ([46486](https://github.com/WordPress/gutenberg/pull/46486)) +- Fix duplication of synced block colors in CSS output. ([46297](https://github.com/WordPress/gutenberg/pull/46297)) +- Make style book label font size 11px. ([46341](https://github.com/WordPress/gutenberg/pull/46341)) +- Style Book: Clear Global Styles navigation history when selecting a block. ([46391](https://github.com/WordPress/gutenberg/pull/46391)) +- - Global Styles REST API endpoint: Check custom CSS is included before attempting to validate. ([46561](https://github.com/WordPress/gutenberg/pull/46561)) +- Reverts the custom CSS to an experiment while we resolve the handling of unfiltered html capabilities. ([46663](https://github.com/WordPress/gutenberg/pull/46663)) + +#### Block Editor +- Block Editor: Fix content locked patterns. ([46494](https://github.com/WordPress/gutenberg/pull/46494)) +- Block Editor: Fix memoized pattern selector dependant arguments. ([46238](https://github.com/WordPress/gutenberg/pull/46238)) +- Block Editor: Restore draggable chip styles. ([46396](https://github.com/WordPress/gutenberg/pull/46396)) +- Block Editor: Revert deoptimization useNestedSettingsUpdate. ([46350](https://github.com/WordPress/gutenberg/pull/46350)) +- Block Editor: Fix some usages of useSelect that return unstable results. ([46226](https://github.com/WordPress/gutenberg/pull/46226)) +- useInnerBlockTemplateSync: Cancel template sync on innerBlocks change or unmount. ([46307](https://github.com/WordPress/gutenberg/pull/46307)) + +#### Patterns +- Add new pattern categories. ([46144](https://github.com/WordPress/gutenberg/pull/46144)) +- Block Editor: Add initial view mode in `BlockPatternSetup`. ([46399](https://github.com/WordPress/gutenberg/pull/46399)) + +#### Site Editor +- Do not remount iframe. ([46431](https://github.com/WordPress/gutenberg/pull/46431)) +- Fix the top bar 'exit' animation. ([46533](https://github.com/WordPress/gutenberg/pull/46533)) +- Keep edited entity in sync when Editor canvas isn't mounted. ([46524](https://github.com/WordPress/gutenberg/pull/46524)) +- [Site Editor]: Add default white background for themes with no `background color` set. ([46314](https://github.com/WordPress/gutenberg/pull/46314)) + +#### Components +- InputControl: Fix `Flex` wrapper usage. ([46213](https://github.com/WordPress/gutenberg/pull/46213)) +- Modal: Fix unexpected modal closing in IME Composition. ([46453](https://github.com/WordPress/gutenberg/pull/46453)) +- MaybeCategoryPanel: Avoid 403 requests for users with low permissions. ([46349](https://github.com/WordPress/gutenberg/pull/46349)) +- Rich text: Add button to clear unknown format. ([44086](https://github.com/WordPress/gutenberg/pull/44086)) + +#### Document Settings +- Fix template title in `summary` panel and requests for low privileged users. ([46304](https://github.com/WordPress/gutenberg/pull/46304)) +- Permalink: Hide edit field for users without publishing capabilities. ([46361](https://github.com/WordPress/gutenberg/pull/46361)) + +#### Patterns +- Content lock: Make filter hook namespace unique. ([46344](https://github.com/WordPress/gutenberg/pull/46344)) + +#### Layout +- Child Layout controls: Fix help text for height. ([46319](https://github.com/WordPress/gutenberg/pull/46319)) + +#### Widgets Editor +- Shortcuts: Add Ctrl+Y for redo to all editor instances on Windows. ([43392](https://github.com/WordPress/gutenberg/pull/43392)) + +#### Block API +- HTML block: Fix parsing. ([27268](https://github.com/WordPress/gutenberg/pull/27268)) + +#### Mobile +- Social Links mobile test: Wait for URL bottom sheet to appear. ([46308](https://github.com/WordPress/gutenberg/pull/46308)) + +### Performance + +#### Components +- Avoid paint on popover when hovering content. ([46201](https://github.com/WordPress/gutenberg/pull/46201)) +- CircularOption: Avoid paint on circular option hover. ([46197](https://github.com/WordPress/gutenberg/pull/46197)) +- Lodash: Replace `_.isEqual()` with `fastDeepEqual`. ([46200](https://github.com/WordPress/gutenberg/pull/46200)) +- Popover: Avoid paint on popovers when scrolling. ([46187](https://github.com/WordPress/gutenberg/pull/46187)) +- Resizable Box: Avoid paint on resizable-box handles. ([46196](https://github.com/WordPress/gutenberg/pull/46196)) +- ListView: Avoid paint on list view item hover. ([46188](https://github.com/WordPress/gutenberg/pull/46188)) + +#### Code Quality +- Lodash: Refactor `blocks` away from `_.find()`. ([46428](https://github.com/WordPress/gutenberg/pull/46428)) +- Lodash: Refactor `core-data` away from `_.find()`. ([46468](https://github.com/WordPress/gutenberg/pull/46468)) +- Lodash: Refactor `edit-site` away from `_.find()`. ([46539](https://github.com/WordPress/gutenberg/pull/46539)) +- Lodash: Refactor away from `_.orderBy()`. ([45146](https://github.com/WordPress/gutenberg/pull/45146)) +- Lodash: Refactor block library away from `_.find()`. ([46430](https://github.com/WordPress/gutenberg/pull/46430)) +- Remove usage of get_default_block_editor_settings. ([46112](https://github.com/WordPress/gutenberg/pull/46112)) + +#### Post Editor +- Lodash: Refactor editor away from `_.find()`. ([46464](https://github.com/WordPress/gutenberg/pull/46464)) +- Lodash: Refactor post editor away from `_.find()`. ([46432](https://github.com/WordPress/gutenberg/pull/46432)) + +#### Block Editor +- Avoid paint on inserter animation. ([46185](https://github.com/WordPress/gutenberg/pull/46185)) +- Improve inserter search performance. ([46153](https://github.com/WordPress/gutenberg/pull/46153)) +- Block Editor: Refactor the "order" state in the block editor reducer to use a map instead of a plain object. ([46221](https://github.com/WordPress/gutenberg/pull/46221)) +- Block Editor: Refactor the block-editor parents state to use maps instead of objects. ([46225](https://github.com/WordPress/gutenberg/pull/46225)) +- Refactor the block-editor "tree" state to use maps instead of objects. ([46229](https://github.com/WordPress/gutenberg/pull/46229)) +- Refactor the block-editor byClientId redux state to use maps instead of plain objects. ([46204](https://github.com/WordPress/gutenberg/pull/46204)) +- Fix typing performance issue for container blocks. ([46527](https://github.com/WordPress/gutenberg/pull/46527)) + +#### Testing +- E2E: Fix performance tests by making inserter search container waiting optional. ([46268](https://github.com/WordPress/gutenberg/pull/46268)) + +#### Mobile +- Columns mobile block: Avoid returning unstable innerWidths from useSelect. ([46403](https://github.com/WordPress/gutenberg/pull/46403)) + +### Experiments + +#### Block Library +- Navigation List View: Remove empty cell when there is no edit button. ([46439](https://github.com/WordPress/gutenberg/pull/46439)) + +#### Web Fonts +- WP Webfonts: Avoid duplicated font families if the font family name was defined using fallback values. ([46378](https://github.com/WordPress/gutenberg/pull/46378)) + +### Documentation +- Adds clarifications and clears up inaccuracies. ([46283](https://github.com/WordPress/gutenberg/pull/46283)) +- Adds details of how to find the .zip file. ([46305](https://github.com/WordPress/gutenberg/pull/46305)) +- Doc: Fix description and documentation for link color support. ([46405](https://github.com/WordPress/gutenberg/pull/46405)) +- Docs: Add missing useState import in BorderBoxControl documentation. ([42067](https://github.com/WordPress/gutenberg/pull/42067)) +- Docs: Add missing useState import in color picker docs. ([42069](https://github.com/WordPress/gutenberg/pull/42069)) +- Docs: Add missing useState import in confirm dialog docs. ([42071](https://github.com/WordPress/gutenberg/pull/42071)) +- Docs: Adds reminder to use Node.js v14 in Quick Start. ([46216](https://github.com/WordPress/gutenberg/pull/46216)) +- Docs: Fix missing link to `primitives` package. ([46290](https://github.com/WordPress/gutenberg/pull/46290)) +- Docs: Update reference to IE 11. ([46296](https://github.com/WordPress/gutenberg/pull/46296)) + +### Code Quality +- Block Editor: Fix `no-node-access` violations in `BlockPreview`. ([46409](https://github.com/WordPress/gutenberg/pull/46409)) +- Block Editor: Fix `no-node-access` violations in `BlockSelectionClearer`. ([46408](https://github.com/WordPress/gutenberg/pull/46408)) +- Columns mobile edit: Remove unused updateBlockSettings action bind. ([46455](https://github.com/WordPress/gutenberg/pull/46455)) +- ESLint: Fix warning in `getBlockAttribute` documentation. ([46500](https://github.com/WordPress/gutenberg/pull/46500)) +- List View: Use default parameters instead of defaultProps. ([46266](https://github.com/WordPress/gutenberg/pull/46266)) +- Removed: Remove small APIs marked to be removed in WP 6.2. ([46106](https://github.com/WordPress/gutenberg/pull/46106)) +- Site Editor: Remove invalid CSS. ([46288](https://github.com/WordPress/gutenberg/pull/46288)) + +#### Block Library +- Group Block: Remove placeholder leftovers. ([46423](https://github.com/WordPress/gutenberg/pull/46423)) +- Group: Remove unnecessary 'useCallback'. ([46418](https://github.com/WordPress/gutenberg/pull/46418)) +- Navigation Block: Add tests for Nav block uncontrolled blocks dirty state checking. ([46329](https://github.com/WordPress/gutenberg/pull/46329)) +- Navigation Block: Update attribute test for `are-blocks-dirty.js`. ([46355](https://github.com/WordPress/gutenberg/pull/46355)) +- Page List Block: Move shared "convert" description to constant. ([46368](https://github.com/WordPress/gutenberg/pull/46368)) +- Page List Block: Simplify Page List convert to links function API. ([46365](https://github.com/WordPress/gutenberg/pull/46365)) +- Query: Cleanup variation picker component. ([46424](https://github.com/WordPress/gutenberg/pull/46424)) +- RNMobile: Add an inline comment to clarify usage of 'hard' limit vs. unbounded query. ([46245](https://github.com/WordPress/gutenberg/pull/46245)) +- Shared standard Link UI component between Nav Link and Submenu blocks. ([46370](https://github.com/WordPress/gutenberg/pull/46370)) +- Template Parts: Remove unnecessary 'useCallback'. ([46420](https://github.com/WordPress/gutenberg/pull/46420)) + +#### Components +- AlignmentMatrixControl: Refactor to TypeScript. ([46162](https://github.com/WordPress/gutenberg/pull/46162)) +- Also ignore `no-node-access` for some components. ([46501](https://github.com/WordPress/gutenberg/pull/46501)) +- Fix `no-node-access` violations in `FocalPointPicker` tests. ([46312](https://github.com/WordPress/gutenberg/pull/46312)) +- Fix `no-node-access` violations in `Popover`. ([46311](https://github.com/WordPress/gutenberg/pull/46311)) +- Fix `no-node-access` violations in `Theme`. ([46310](https://github.com/WordPress/gutenberg/pull/46310)) +- Fix `no-node-access` violations in `ToolsPanel` tests. ([46313](https://github.com/WordPress/gutenberg/pull/46313)) +- withFilters: Use 'act' from React Testing Library. ([46237](https://github.com/WordPress/gutenberg/pull/46237)) + +#### Data Layer +- Data: Add ability to subscribe to one store, remove __unstableSubscribeStore. ([45513](https://github.com/WordPress/gutenberg/pull/45513)) +- ESLint: Fix warnings in the data package. ([46499](https://github.com/WordPress/gutenberg/pull/46499)) + +#### Global Styles +- Add "custom-css" as an acceptable value in the documentation for gutenberg_get_global_stylesheet. ([46493](https://github.com/WordPress/gutenberg/pull/46493)) +- PaletteEdit: Add changelog. ([46095](https://github.com/WordPress/gutenberg/pull/46095)) + +#### Block Editor +- Inserter: Update mobile tab navigation styles. ([46186](https://github.com/WordPress/gutenberg/pull/46186)) + +#### Layout +- Clarify inline comment about switching to `safecss_filter_attr`. ([46061](https://github.com/WordPress/gutenberg/pull/46061)) + +### Tools + +#### Build Tooling +- Adds Github Action to validate Gradle Wrapper. ([46247](https://github.com/WordPress/gutenberg/pull/46247)) +- Prevent api-fetch and core-data from being imported in the block editor package. ([46302](https://github.com/WordPress/gutenberg/pull/46302)) +- Serialize the map objects properly in the Redux dev tools. ([46282](https://github.com/WordPress/gutenberg/pull/46282)) + +#### Testing +- E2E: Fix flaky Block Switcher tests. ([46406](https://github.com/WordPress/gutenberg/pull/46406)) +- end-to-end tests: Add width and color test to button block. ([46452](https://github.com/WordPress/gutenberg/pull/46452)) + + + + +## Contributors + +The following contributors merged PRs in this release: + +@andrewserong @glendaviesnz + + += 14.8.0-rc.1 = + +## Changelog + +### Enhancements + +#### Block Library +- Add a current-menu-ancestor class to navigation items. ([40778](https://github.com/WordPress/gutenberg/pull/40778)) +- Page List Block: Adds a longdash tree to the parent selector. ([46336](https://github.com/WordPress/gutenberg/pull/46336)) +- Page List Block: Hide page list edit button if no pages are available. ([46331](https://github.com/WordPress/gutenberg/pull/46331)) +- Reusable block: Pluralize the message "Convert to regular blocks" depending on the number of blocks contained. ([45819](https://github.com/WordPress/gutenberg/pull/45819)) +- Add page list to Link UI transforms in Nav block. ([46426](https://github.com/WordPress/gutenberg/pull/46426)) +- Heading Block: Don't rely on the experimental selector anymore. ([46284](https://github.com/WordPress/gutenberg/pull/46284)) +- Media & Text Block: Create undo history when media width is changed. ([46084](https://github.com/WordPress/gutenberg/pull/46084)) +- Navigation block: Add location->primary to fallback nav creation for classic menus. ([45976](https://github.com/WordPress/gutenberg/pull/45976)) +- Navigation block: Update fallback nav creation to the most recently created menu. ([46286](https://github.com/WordPress/gutenberg/pull/46286)) +- Navigation: Add a 'open list view' button. ([46335](https://github.com/WordPress/gutenberg/pull/46335)) +- Navigation: Removes the header from the navigation list view in the experiment. ([46070](https://github.com/WordPress/gutenberg/pull/46070)) +- Page List: Add convert panel to Inspector Controls when within Nav block. ([46352](https://github.com/WordPress/gutenberg/pull/46352)) +- Page List: Prevent users from adding inner blocks to Page List. ([46269](https://github.com/WordPress/gutenberg/pull/46269)) +- Query: Remove color block supports. ([46147](https://github.com/WordPress/gutenberg/pull/46147)) +- Table block: Make `figcaption` styles consistent between editor and front end. ([46172](https://github.com/WordPress/gutenberg/pull/46172)) +- List/quote: Unwrap inner block when pressing Backspace at start. ([45075](https://github.com/WordPress/gutenberg/pull/45075)) + +#### Inspector Controls +- Sidebar Tabs: Refine the use of inspector tabs and disable filters for Nav blocks. ([46346](https://github.com/WordPress/gutenberg/pull/46346)) +- Sidebar Tabs: Use editor settings to override display. ([46321](https://github.com/WordPress/gutenberg/pull/46321)) +- Summary panel: Try improving spacing and grid. ([46267](https://github.com/WordPress/gutenberg/pull/46267)) + +#### Global Styles +- Add Style Book to Global Styles. ([45960](https://github.com/WordPress/gutenberg/pull/45960)) +- Add block preview component in global styles. ([45719](https://github.com/WordPress/gutenberg/pull/45719)) +- Move border from layout to own menu. ([45995](https://github.com/WordPress/gutenberg/pull/45995)) +- Add a css style to theme.json to allow setting of custom css strings. ([46255](https://github.com/WordPress/gutenberg/pull/46255)) +- Expose before filter hook in useSettings for injecting block settings in the editor. ([45089](https://github.com/WordPress/gutenberg/pull/45089)) +- Global styles: Add custom CSS panel to site editor. ([46141](https://github.com/WordPress/gutenberg/pull/46141)) + +#### Site Editor +- Allow adding new templates and template parts directly from the sidebar. ([46458](https://github.com/WordPress/gutenberg/pull/46458)) +- Synchronize the sidebar state in the URL. ([46433](https://github.com/WordPress/gutenberg/pull/46433)) +- Try template drill down on the shell sidebar (browse mode). ([45100](https://github.com/WordPress/gutenberg/pull/45100)) + +#### Block Editor +- Update the synced block hover styles in Inserter. ([46442](https://github.com/WordPress/gutenberg/pull/46442)) +- Add new selector getLastInsertedBlockClientId. ([46531](https://github.com/WordPress/gutenberg/pull/46531)) +- Block editor: Hide fixed contextual toolbar. ([46298](https://github.com/WordPress/gutenberg/pull/46298)) +- Inserter: Pattern title tooltip. ([46419](https://github.com/WordPress/gutenberg/pull/46419)) +- useNestedSettingsUpdate: Prevent unneeded syncing of falsy templateLock values. ([46357](https://github.com/WordPress/gutenberg/pull/46357)) +- Design: Augmented shadows for modals and popovers. ([46228](https://github.com/WordPress/gutenberg/pull/46228)) + +#### Components +- Tabs: Try a simpler tab focus style, alt. ([46276](https://github.com/WordPress/gutenberg/pull/46276)) +- BaseControl: Add convenience hook to generate id-related props. ([46170](https://github.com/WordPress/gutenberg/pull/46170)) +- Dashicon: Refactor to TypeScript. ([45924](https://github.com/WordPress/gutenberg/pull/45924)) +- Lighten borders to gray-600. ([46252](https://github.com/WordPress/gutenberg/pull/46252)) +- Popover: Check positioning by adding and testing is-positioned class. ([46429](https://github.com/WordPress/gutenberg/pull/46429)) + +### Icons +- Icons: Update the border icon. ([46264](https://github.com/WordPress/gutenberg/pull/46264)) + +#### Testing +- Tests: Fix `toBePositionedPopover` matcher message function. ([46239](https://github.com/WordPress/gutenberg/pull/46239)) + +#### Accessibility +- Reorganize the site editor to introduce Browse Mode. ([44770](https://github.com/WordPress/gutenberg/pull/44770)) + +#### Plugin +- Update the Gutenberg plugin to require at least the WP 6.0 version. ([46102](https://github.com/WordPress/gutenberg/pull/46102)) +- PHP: Backport changes from core theme resolver. ([46250](https://github.com/WordPress/gutenberg/pull/46250)) +- Update: Move gutenberg_register_core_block_patterns from 6.1 to 6.2. ([46249](https://github.com/WordPress/gutenberg/pull/46249)) +- Upgrade React packages to v18. ([45235](https://github.com/WordPress/gutenberg/pull/45235)) + +#### Themes +- Empty Theme: Add the `$schema` property in `theme.json` and rename template directories. ([46300](https://github.com/WordPress/gutenberg/pull/46300)) + +#### Mobile +- Mobile: Disable Unsupported Block Editor Tests (Android). ([46542](https://github.com/WordPress/gutenberg/pull/46542)) +- Mobile: Inserter - Remove `.done()` usage. ([46460](https://github.com/WordPress/gutenberg/pull/46460)) +- Mobile: Update Heading block end-to-end test. ([46220](https://github.com/WordPress/gutenberg/pull/46220)) +- Mobile: Updates packages to not use Git HTTPS URLs. ([46422](https://github.com/WordPress/gutenberg/pull/46422)) + +### Bug Fixes + +#### Block Library +- Fix Nav Submenu block Link UI text control. ([46243](https://github.com/WordPress/gutenberg/pull/46243)) +- Fix auto Nav menu creation due to page list inner blocks. ([46223](https://github.com/WordPress/gutenberg/pull/46223)) +- Handle innerContent too when removing innerBlocks. ([46377](https://github.com/WordPress/gutenberg/pull/46377)) +- Image Block: Ensure drag handle matches cursor position when resizing a center aligned image. ([46497](https://github.com/WordPress/gutenberg/pull/46497)) +- Navigation Block: Add social link singular to list of blocks to be allowed. ([46374](https://github.com/WordPress/gutenberg/pull/46374)) +- Navigation Block: Fixes adding a submenu. ([46364](https://github.com/WordPress/gutenberg/pull/46364)) +- Navigation Block: Prevent circular references in navigation block rendering. ([46387](https://github.com/WordPress/gutenberg/pull/46387)) +- Navigation Block: Recursively remove Navigation block’s from appearing inside Navigation block on front of site. ([46279](https://github.com/WordPress/gutenberg/pull/46279)) +- Navigation link: Use stripHTML. ([46317](https://github.com/WordPress/gutenberg/pull/46317)) +- Page List Block: Fix error loading page list parent options. ([46327](https://github.com/WordPress/gutenberg/pull/46327)) +- Query Loop Block: Add migration of colors to v2 deprecation. ([46522](https://github.com/WordPress/gutenberg/pull/46522)) +- Site Logo: Correctly set the image's natural height and width. ([46214](https://github.com/WordPress/gutenberg/pull/46214)) +- Strip markup from link label data in inspector. ([46171](https://github.com/WordPress/gutenberg/pull/46171)) +- Template Parts: Fix modal search stacking context. ([46421](https://github.com/WordPress/gutenberg/pull/46421)) +- Video: Avoid an error when removal is locked. ([46324](https://github.com/WordPress/gutenberg/pull/46324)) +- Layout child fixed size should not be fixed by default and should always have a value set. ([46139](https://github.com/WordPress/gutenberg/pull/46139)) + +#### Blocks +- Paste handler: Remove styles on inline paste. ([46402](https://github.com/WordPress/gutenberg/pull/46402)) +- Improve performance of gutenberg_render_layout_support_flag. ([46074](https://github.com/WordPress/gutenberg/pull/46074)) + +#### Global Styles +- Allow indirect properties when unfiltered_html is not allowed. ([46388](https://github.com/WordPress/gutenberg/pull/46388)) +- Fix Reset to defaults action by moving fills to be within context provider. ([46486](https://github.com/WordPress/gutenberg/pull/46486)) +- Fix duplication of synced block colors in CSS output. ([46297](https://github.com/WordPress/gutenberg/pull/46297)) +- Make style book label font size 11px. ([46341](https://github.com/WordPress/gutenberg/pull/46341)) +- Style Book: Clear Global Styles navigation history when selecting a block. ([46391](https://github.com/WordPress/gutenberg/pull/46391)) +- Global Styles REST API endpoint: Check custom CSS is included before attempting to validate. ([46561](https://github.com/WordPress/gutenberg/pull/46561)) +- Reverts the custom CSS to an experiment while we resolve the handling of unfiltered html capabilities. ([46663](https://github.com/WordPress/gutenberg/pull/46663)) + +#### Block Editor +- Block Editor: Fix content locked patterns. ([46494](https://github.com/WordPress/gutenberg/pull/46494)) +- Block Editor: Fix memoized pattern selector dependant arguments. ([46238](https://github.com/WordPress/gutenberg/pull/46238)) +- Block Editor: Restore draggable chip styles. ([46396](https://github.com/WordPress/gutenberg/pull/46396)) +- Block Editor: Revert deoptimization useNestedSettingsUpdate. ([46350](https://github.com/WordPress/gutenberg/pull/46350)) +- Block Editor: Fix some usages of useSelect that return unstable results. ([46226](https://github.com/WordPress/gutenberg/pull/46226)) +- useInnerBlockTemplateSync: Cancel template sync on innerBlocks change or unmount. ([46307](https://github.com/WordPress/gutenberg/pull/46307)) + +#### Patterns +- Add new pattern categories. ([46144](https://github.com/WordPress/gutenberg/pull/46144)) +- Block Editor: Add initial view mode in `BlockPatternSetup`. ([46399](https://github.com/WordPress/gutenberg/pull/46399)) + +#### Site Editor +- Do not remount iframe. ([46431](https://github.com/WordPress/gutenberg/pull/46431)) +- Fix the top bar 'exit' animation. ([46533](https://github.com/WordPress/gutenberg/pull/46533)) +- Keep edited entity in sync when Editor canvas isn't mounted. ([46524](https://github.com/WordPress/gutenberg/pull/46524)) +- [Site Editor]: Add default white background for themes with no `background color` set. ([46314](https://github.com/WordPress/gutenberg/pull/46314)) + +#### Components +- InputControl: Fix `Flex` wrapper usage. ([46213](https://github.com/WordPress/gutenberg/pull/46213)) +- Modal: Fix unexpected modal closing in IME Composition. ([46453](https://github.com/WordPress/gutenberg/pull/46453)) +- MaybeCategoryPanel: Avoid 403 requests for users with low permissions. ([46349](https://github.com/WordPress/gutenberg/pull/46349)) +- Rich text: Add button to clear unknown format. ([44086](https://github.com/WordPress/gutenberg/pull/44086)) + +#### Document Settings +- Fix template title in `summary` panel and requests for low privileged users. ([46304](https://github.com/WordPress/gutenberg/pull/46304)) +- Permalink: Hide edit field for users without publishing capabilities. ([46361](https://github.com/WordPress/gutenberg/pull/46361)) + +#### Patterns +- Content lock: Make filter hook namespace unique. ([46344](https://github.com/WordPress/gutenberg/pull/46344)) + +#### Layout +- Child Layout controls: Fix help text for height. ([46319](https://github.com/WordPress/gutenberg/pull/46319)) + +#### Widgets Editor +- Shortcuts: Add Ctrl+Y for redo to all editor instances on Windows. ([43392](https://github.com/WordPress/gutenberg/pull/43392)) + +#### Block API +- HTML block: Fix parsing. ([27268](https://github.com/WordPress/gutenberg/pull/27268)) + +#### Mobile +- Social Links mobile test: Wait for URL bottom sheet to appear. ([46308](https://github.com/WordPress/gutenberg/pull/46308)) + +### Performance + +#### Components +- Avoid paint on popover when hovering content. ([46201](https://github.com/WordPress/gutenberg/pull/46201)) +- CircularOption: Avoid paint on circular option hover. ([46197](https://github.com/WordPress/gutenberg/pull/46197)) +- Lodash: Replace `_.isEqual()` with `fastDeepEqual`. ([46200](https://github.com/WordPress/gutenberg/pull/46200)) +- Popover: Avoid paint on popovers when scrolling. ([46187](https://github.com/WordPress/gutenberg/pull/46187)) +- Resizable Box: Avoid paint on resizable-box handles. ([46196](https://github.com/WordPress/gutenberg/pull/46196)) +- ListView: Avoid paint on list view item hover. ([46188](https://github.com/WordPress/gutenberg/pull/46188)) + +#### Code Quality +- Lodash: Refactor `blocks` away from `_.find()`. ([46428](https://github.com/WordPress/gutenberg/pull/46428)) +- Lodash: Refactor `core-data` away from `_.find()`. ([46468](https://github.com/WordPress/gutenberg/pull/46468)) +- Lodash: Refactor `edit-site` away from `_.find()`. ([46539](https://github.com/WordPress/gutenberg/pull/46539)) +- Lodash: Refactor away from `_.orderBy()`. ([45146](https://github.com/WordPress/gutenberg/pull/45146)) +- Lodash: Refactor block library away from `_.find()`. ([46430](https://github.com/WordPress/gutenberg/pull/46430)) +- Remove usage of get_default_block_editor_settings. ([46112](https://github.com/WordPress/gutenberg/pull/46112)) + +#### Post Editor +- Lodash: Refactor editor away from `_.find()`. ([46464](https://github.com/WordPress/gutenberg/pull/46464)) +- Lodash: Refactor post editor away from `_.find()`. ([46432](https://github.com/WordPress/gutenberg/pull/46432)) + +#### Block Editor +- Avoid paint on inserter animation. ([46185](https://github.com/WordPress/gutenberg/pull/46185)) +- Improve inserter search performance. ([46153](https://github.com/WordPress/gutenberg/pull/46153)) +- Block Editor: Refactor the "order" state in the block editor reducer to use a map instead of a plain object. ([46221](https://github.com/WordPress/gutenberg/pull/46221)) +- Block Editor: Refactor the block-editor parents state to use maps instead of objects. ([46225](https://github.com/WordPress/gutenberg/pull/46225)) +- Refactor the block-editor "tree" state to use maps instead of objects. ([46229](https://github.com/WordPress/gutenberg/pull/46229)) +- Refactor the block-editor byClientId redux state to use maps instead of plain objects. ([46204](https://github.com/WordPress/gutenberg/pull/46204)) +- Fix typing performance issue for container blocks. ([46527](https://github.com/WordPress/gutenberg/pull/46527)) + +#### Testing +- E2E: Fix performance tests by making inserter search container waiting optional. ([46268](https://github.com/WordPress/gutenberg/pull/46268)) + +#### Mobile +- Columns mobile block: Avoid returning unstable innerWidths from useSelect. ([46403](https://github.com/WordPress/gutenberg/pull/46403)) + +### Experiments + +#### Block Library +- Navigation List View: Remove empty cell when there is no edit button. ([46439](https://github.com/WordPress/gutenberg/pull/46439)) + +#### Web Fonts +- WP Webfonts: Avoid duplicated font families if the font family name was defined using fallback values. ([46378](https://github.com/WordPress/gutenberg/pull/46378)) + +### Documentation +- Adds clarifications and clears up inaccuracies. ([46283](https://github.com/WordPress/gutenberg/pull/46283)) +- Adds details of how to find the .zip file. ([46305](https://github.com/WordPress/gutenberg/pull/46305)) +- Doc: Fix description and documentation for link color support. ([46405](https://github.com/WordPress/gutenberg/pull/46405)) +- Docs: Add missing useState import in BorderBoxControl documentation. ([42067](https://github.com/WordPress/gutenberg/pull/42067)) +- Docs: Add missing useState import in color picker docs. ([42069](https://github.com/WordPress/gutenberg/pull/42069)) +- Docs: Add missing useState import in confirm dialog docs. ([42071](https://github.com/WordPress/gutenberg/pull/42071)) +- Docs: Adds reminder to use Node.js v14 in Quick Start. ([46216](https://github.com/WordPress/gutenberg/pull/46216)) +- Docs: Fix missing link to `primitives` package. ([46290](https://github.com/WordPress/gutenberg/pull/46290)) +- Docs: Update reference to IE 11. ([46296](https://github.com/WordPress/gutenberg/pull/46296)) + +### Code Quality +- Block Editor: Fix `no-node-access` violations in `BlockPreview`. ([46409](https://github.com/WordPress/gutenberg/pull/46409)) +- Block Editor: Fix `no-node-access` violations in `BlockSelectionClearer`. ([46408](https://github.com/WordPress/gutenberg/pull/46408)) +- Columns mobile edit: Remove unused updateBlockSettings action bind. ([46455](https://github.com/WordPress/gutenberg/pull/46455)) +- ESLint: Fix warning in `getBlockAttribute` documentation. ([46500](https://github.com/WordPress/gutenberg/pull/46500)) +- List View: Use default parameters instead of defaultProps. ([46266](https://github.com/WordPress/gutenberg/pull/46266)) +- Removed: Remove small APIs marked to be removed in WP 6.2. ([46106](https://github.com/WordPress/gutenberg/pull/46106)) +- Site Editor: Remove invalid CSS. ([46288](https://github.com/WordPress/gutenberg/pull/46288)) + +#### Block Library +- Group Block: Remove placeholder leftovers. ([46423](https://github.com/WordPress/gutenberg/pull/46423)) +- Group: Remove unnecessary 'useCallback'. ([46418](https://github.com/WordPress/gutenberg/pull/46418)) +- Navigation Block: Add tests for Nav block uncontrolled blocks dirty state checking. ([46329](https://github.com/WordPress/gutenberg/pull/46329)) +- Navigation Block: Update attribute test for `are-blocks-dirty.js`. ([46355](https://github.com/WordPress/gutenberg/pull/46355)) +- Page List Block: Move shared "convert" description to constant. ([46368](https://github.com/WordPress/gutenberg/pull/46368)) +- Page List Block: Simplify Page List convert to links function API. ([46365](https://github.com/WordPress/gutenberg/pull/46365)) +- Query: Cleanup variation picker component. ([46424](https://github.com/WordPress/gutenberg/pull/46424)) +- RNMobile: Add an inline comment to clarify usage of 'hard' limit vs. unbounded query. ([46245](https://github.com/WordPress/gutenberg/pull/46245)) +- Shared standard Link UI component between Nav Link and Submenu blocks. ([46370](https://github.com/WordPress/gutenberg/pull/46370)) +- Template Parts: Remove unnecessary 'useCallback'. ([46420](https://github.com/WordPress/gutenberg/pull/46420)) + +#### Components +- AlignmentMatrixControl: Refactor to TypeScript. ([46162](https://github.com/WordPress/gutenberg/pull/46162)) +- Also ignore `no-node-access` for some components. ([46501](https://github.com/WordPress/gutenberg/pull/46501)) +- Fix `no-node-access` violations in `FocalPointPicker` tests. ([46312](https://github.com/WordPress/gutenberg/pull/46312)) +- Fix `no-node-access` violations in `Popover`. ([46311](https://github.com/WordPress/gutenberg/pull/46311)) +- Fix `no-node-access` violations in `Theme`. ([46310](https://github.com/WordPress/gutenberg/pull/46310)) +- Fix `no-node-access` violations in `ToolsPanel` tests. ([46313](https://github.com/WordPress/gutenberg/pull/46313)) +- withFilters: Use 'act' from React Testing Library. ([46237](https://github.com/WordPress/gutenberg/pull/46237)) + +#### Data Layer +- Data: Add ability to subscribe to one store, remove __unstableSubscribeStore. ([45513](https://github.com/WordPress/gutenberg/pull/45513)) +- ESLint: Fix warnings in the data package. ([46499](https://github.com/WordPress/gutenberg/pull/46499)) + +#### Global Styles +- Add "custom-css" as an acceptable value in the documentation for gutenberg_get_global_stylesheet. ([46493](https://github.com/WordPress/gutenberg/pull/46493)) +- PaletteEdit: Add changelog. ([46095](https://github.com/WordPress/gutenberg/pull/46095)) + +#### Block Editor +- Inserter: Update mobile tab navigation styles. ([46186](https://github.com/WordPress/gutenberg/pull/46186)) + +#### Layout +- Clarify inline comment about switching to `safecss_filter_attr`. ([46061](https://github.com/WordPress/gutenberg/pull/46061)) + +### Tools + +#### Build Tooling +- Adds Github Action to validate Gradle Wrapper. ([46247](https://github.com/WordPress/gutenberg/pull/46247)) +- Prevent api-fetch and core-data from being imported in the block editor package. ([46302](https://github.com/WordPress/gutenberg/pull/46302)) +- Serialize the map objects properly in the Redux dev tools. ([46282](https://github.com/WordPress/gutenberg/pull/46282)) + +#### Testing +- E2E: Fix flaky Block Switcher tests. ([46406](https://github.com/WordPress/gutenberg/pull/46406)) +- end-to-end tests: Add width and color test to button block. ([46452](https://github.com/WordPress/gutenberg/pull/46452)) + + +## First time contributors + +The following PRs were merged by first time contributors: + +- @corentin-gautier: Avoid paint on popover when hovering content. ([46201](https://github.com/WordPress/gutenberg/pull/46201)) +- @ingeniumed: Expose before filter hook in useSettings for injecting block settings in the editor. ([45089](https://github.com/WordPress/gutenberg/pull/45089)) +- @janusqa: Reusable block: Pluralize the message "Convert to regular blocks" depending on the number of blocks contained. ([45819](https://github.com/WordPress/gutenberg/pull/45819)) + + +## Contributors + +The following contributors merged PRs in this release: + +@aaronrobertshaw @ajlende @andrewserong @aristath @chad1008 @chintu51 @corentin-gautier @derekblank @draganescu @ellatrix @geriux @getdave @glendaviesnz @hideokamoto @ingeniumed @jameskoster @janusqa @jasmussen @jffng @jorgefilipecosta @jsnajdr @madhusudhand @MaggieCabrera @Mamaduka @matiasbenedetto @mburridge @mikachan @mirka @noisysocks @ntsekouras @oandregal @oguzkocer @ramonjd @scruffian @SiobhyB @spacedmonkey @t-hamano @talldan @tellthemachines @tyxla @WunderBart @youknowriad + + = 14.7.3 = ## Changelog diff --git a/docs/README.md b/docs/README.md index 1fe569f7356cbe..4d5343a76141e9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -29,7 +29,7 @@ The Editor offers rich new value to users with visual, drag-and-drop creation to Whether you want to extend the functionality of the block editor, or create a plugin based on it, [see the developer documentation](/docs/how-to-guides/README.md) to find all the information about the basic concepts you need to get started, the block editor APIs and its architecture. - [Gutenberg Architecture](/docs/explanations/architecture/README.md) -- [Block Styles](/docs/reference-guides/filters/block-filters.md#block-styles) +- [Block Styles](/docs/reference-guides/block-api/block-styles.md) - [Creating Block Patterns](/docs/reference-guides/block-api/block-patterns.md) - [Theming for the Block Editor](/docs/how-to-guides/themes/README.md) - [Block API Reference](/docs/reference-guides/block-api/README.md) diff --git a/docs/contributors/code/release.md b/docs/contributors/code/release.md index f463cb319f6657..492813376b2c9d 100644 --- a/docs/contributors/code/release.md +++ b/docs/contributors/code/release.md @@ -34,6 +34,10 @@ To release a release candidate (RC) version of the plugin, enter `rc`. To releas This will trigger a GitHub Actions (GHA) workflow that bumps the plugin version, builds the Gutenberg plugin .zip file, creates a release draft, and attaches the plugin .zip file to it. This part of the process typically takes a little under six minutes. You'll see that workflow appear at the top of the list, right under the blue banner. Once it's finished, it'll change its status icon from a yellow dot to a green checkmark. You can follow along in a more detailed view by clicking on the workflow. +#### Publishing the @wordpress packages to NPM + +As part of the release candidate (RC) process, all of the `@wordpress` packages are published to NPM. You may see messaging after the ["Build Gutenberg Plugin Zip" action](https://github.com/WordPress/gutenberg/actions/workflows/build-plugin-zip.yml) action has created the draft release that the ["Publish npm packages"](https://github.com/WordPress/gutenberg/actions/workflows/publish-npm-packages.yml) action requires someone with appropriate permissions to trigger the action. This is not the case as this process is automated and it will automatically run after the release notes are published. + #### View the release draft As soon as the workflow has finished, you'll find the release draft under [https://github.com/WordPress/gutenberg/releases](https://github.com/WordPress/gutenberg/releases). The draft is pre-populated with changelog entries based on previous release candidates for this version, and any changes that have since been cherry-picked to the release branch. Thus, when releasing the first stable version of a series, make sure to delete any RC version headers (that are only there for your information) and to move the more recent changes to the correct section (see below). diff --git a/docs/contributors/code/scripts.md b/docs/contributors/code/scripts.md index 3b36c8f4be4956..b7cabe0130d71c 100644 --- a/docs/contributors/code/scripts.md +++ b/docs/contributors/code/scripts.md @@ -31,7 +31,6 @@ The editor includes a number of packages to enable various pieces of functionali | [Is Shallow Equal](/packages/is-shallow-equal/README.md) | wp-is-shallow-equal | A function for performing a shallow comparison between two objects or arrays | | [Keycodes](/packages/keycodes/README.md) | wp-keycodes | Keycodes utilities for WordPress, used to check the key pressed in events like `onKeyDown` | | [List Reusable blocks](/packages/list-reusable-blocks/README.md) | wp-list-reusable-blocks | Package used to add import/export links to the listing page of the reusable blocks | -| [NUX](/packages/nux/README.md) | wp-nux | Components, and wp.data methods useful for onboarding a new user to the WordPress admin interface | | [Plugins](/packages/plugins/README.md) | wp-plugins | Plugins module for WordPress | | [Redux Routine](/packages/redux-routine/README.md) | wp-redux-routine | Redux middleware for generator coroutines | | [Rich Text](/packages/rich-text/README.md) | wp-rich-text | Helper functions to convert HTML or a DOM tree into a rich text value and back | diff --git a/docs/how-to-guides/block-tutorial/extending-the-query-loop-block.md b/docs/how-to-guides/block-tutorial/extending-the-query-loop-block.md index e43dcb9727088d..cc4d0b1ee0a0d7 100644 --- a/docs/how-to-guides/block-tutorial/extending-the-query-loop-block.md +++ b/docs/how-to-guides/block-tutorial/extending-the-query-loop-block.md @@ -119,6 +119,12 @@ The Query Loop block determines if there is an active variation of itself and if In order for a pattern to be “connected” with a Query Loop variation, you should add the name of your variation prefixed with the Query Loop name (e.g. `core/query/$variation_name`) to the pattern's `blockTypes` property. For more details about registering patterns [see here](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-patterns/). +If you have not provided `innerBlocks` in your variation, there is also a way to suggest “connected” variations when the user selects `Start blank` in the setup phase. This is handled in a similar fashion with “connected” patterns, by checking if there is an active variation of Query Loop and if there are any connected variations to suggest. + +In order for a variation to be connected to another Query Loop variation we need to define the `scope` attribute with `['block']` as value and the `namespace` attribute defined as an array. This array should contain the names(`name` property) of any variations they want to be connected to. + +For example, if we have a Query Loop variation exposed to the inserter(`scope: ['inserter']`) with the name `products`, we can connect a scoped `block` variation by setting its `namespace` attribute to `['products']`. If the user selects this variation after having clicked `Start blank`, the namespace attribute will be overridden by the main inserter variation. + ### Making Gutenberg recognize your variation There is one slight problem you might have realized after implementing this variation: while it is transparent to the user as they are inserting it, Gutenberg will still recognize the variation as a Query Loop block at its core and so, after its insertion, it will show up as a Query Loop block in the tree view of the editor, for instance. diff --git a/docs/how-to-guides/javascript/extending-the-block-editor.md b/docs/how-to-guides/javascript/extending-the-block-editor.md index 75442628f83ac5..5944b9208407f5 100644 --- a/docs/how-to-guides/javascript/extending-the-block-editor.md +++ b/docs/how-to-guides/javascript/extending-the-block-editor.md @@ -1,6 +1,6 @@ # Extending the Block Editor -Let's look at using the [Block Style example](/docs/reference-guides/filters/block-filters.md#block-styles) to extend the editor. This example allows you to add your own custom CSS class name to any core block type. +Let's look at using the [Block Style example](/docs/reference-guides/block-api/block-styles.md) to extend the editor. This example allows you to add your own custom CSS class name to any core block type. Replace the existing `console.log()` code in your `myguten.js` file with: diff --git a/docs/manifest.json b/docs/manifest.json index 8cc39af57531e4..a06df18c6c44b9 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1733,12 +1733,6 @@ "markdown_source": "../packages/npm-package-json-lint-config/README.md", "parent": "packages" }, - { - "title": "@wordpress/nux", - "slug": "packages-nux", - "markdown_source": "../packages/nux/README.md", - "parent": "packages" - }, { "title": "@wordpress/plugins", "slug": "packages-plugins", @@ -1973,12 +1967,6 @@ "markdown_source": "../docs/reference-guides/data/data-core-notices.md", "parent": "data" }, - { - "title": "The NUX (New User Experience) Data", - "slug": "data-core-nux", - "markdown_source": "../docs/reference-guides/data/data-core-nux.md", - "parent": "data" - }, { "title": "Preferences", "slug": "data-core-preferences", diff --git a/docs/reference-guides/README.md b/docs/reference-guides/README.md index 33fdd9aa602414..f13c838697f2de 100644 --- a/docs/reference-guides/README.md +++ b/docs/reference-guides/README.md @@ -63,7 +63,6 @@ - [**core/editor**: The Post Editor’s Data](/docs/reference-guides/data/data-core-editor.md) - [**core/keyboard-shortcuts**: The Keyboard Shortcuts Data](/docs/reference-guides/data/data-core-keyboard-shortcuts.md) - [**core/notices**: Notices Data](/docs/reference-guides/data/data-core-notices.md) - - [**core/nux**: The NUX (New User Experience) Data](/docs/reference-guides/data/data-core-nux.md) - [**core/preferences**: Preferences](/docs/reference-guides/data/data-core-preferences.md) - [**core/reusable-blocks**: Reusable blocks](/docs/reference-guides/data/data-core-reusable-blocks.md) - [**core/rich-text**: Rich Text](/docs/reference-guides/data/data-core-rich-text.md) diff --git a/docs/reference-guides/block-api/block-deprecation.md b/docs/reference-guides/block-api/block-deprecation.md index 1bb24694749a0f..0314939ee8f649 100644 --- a/docs/reference-guides/block-api/block-deprecation.md +++ b/docs/reference-guides/block-api/block-deprecation.md @@ -234,7 +234,6 @@ E.g: a block wants to migrate a title attribute to a paragraph innerBlock. ```js const { registerBlockType } = wp.blocks; -const { omit } = lodash; registerBlockType( 'gutenberg/block-with-deprecated-version', { // ... block properties go here @@ -254,8 +253,10 @@ registerBlockType( 'gutenberg/block-with-deprecated-version', { }, migrate( attributes, innerBlocks ) { + const { title, ...restAttributes } = attributes; + return [ - omit( attributes, 'title' ), + restAttributes, [ createBlock( 'core/paragraph', { content: attributes.title, @@ -278,8 +279,7 @@ registerBlockType( 'gutenberg/block-with-deprecated-version', { ```js var el = wp.element.createElement, - registerBlockType = wp.blocks.registerBlockType, - omit = lodash.omit; + registerBlockType = wp.blocks.registerBlockType; registerBlockType( 'gutenberg/block-with-deprecated-version', { // ... block properties go here @@ -295,8 +295,10 @@ registerBlockType( 'gutenberg/block-with-deprecated-version', { }, migrate: function ( attributes, innerBlocks ) { + const { title, ...restAttributes } = attributes; + return [ - omit( attributes, 'title' ), + restAttributes, [ createBlock( 'core/paragraph', { content: attributes.title, diff --git a/docs/reference-guides/block-api/block-metadata.md b/docs/reference-guides/block-api/block-metadata.md index 4acb0f914e0379..d78050d79a9a1e 100644 --- a/docs/reference-guides/block-api/block-metadata.md +++ b/docs/reference-guides/block-api/block-metadata.md @@ -408,7 +408,7 @@ It contains as set of options to control features used in the editor. See the [t Block styles can be used to provide alternative styles to block. It works by adding a class name to the block's wrapper. Using CSS, a theme developer can target the class name for the block style if it is selected. -Plugins and Themes can also register [custom block style](/docs/reference-guides/filters/block-filters.md#block-styles) for existing blocks. +Plugins and Themes can also register [custom block style](/docs/reference-guides/block-api/block-styles.md) for existing blocks. ### Example diff --git a/docs/reference-guides/block-api/block-patterns.md b/docs/reference-guides/block-api/block-patterns.md index 12fac7b479361a..d58e7a2f62d41c 100644 --- a/docs/reference-guides/block-api/block-patterns.md +++ b/docs/reference-guides/block-api/block-patterns.md @@ -34,6 +34,7 @@ The properties available for block patterns are: - `viewportWidth` (optional): An integer specifying the intended width of the pattern to allow for a scaled preview of the pattern in the inserter. - `blockTypes` (optional): An array of block types that the pattern is intended to be used with. Each value needs to be the declared block's `name`. - `postTypes` (optional): An array of post types that the pattern is restricted to be used with. The pattern will only be available when editing one of the post types passed on the array, for all the other post types the pattern is not available at all. +- `templateTypes` (optional): An array of template types where the pattern makes sense e.g: '404' if the pattern is for a 404 page, single-post if the pattern is for showing a single post. - `inserter` (optional): By default, all patterns will appear in the inserter. To hide a pattern so that it can only be inserted programmatically, set the `inserter` to `false`. The following code sample registers a block pattern named 'my-plugin/my-awesome-pattern': @@ -113,6 +114,8 @@ _Note:_ `register_block_pattern_category()` should be called from a handler attached to the init hook. +The category will not show under Patterns unless a pattern has been assigned to that category. + ```php function my_plugin_register_my_pattern_categories() { register_block_pattern_category( ... ); diff --git a/docs/reference-guides/block-api/block-registration.md b/docs/reference-guides/block-api/block-registration.md index 97077cb7efdfaa..bae50ac67a0fd3 100644 --- a/docs/reference-guides/block-api/block-registration.md +++ b/docs/reference-guides/block-api/block-registration.md @@ -145,7 +145,7 @@ styles: [ ], ``` -Plugins and Themes can also register [custom block style](/docs/reference-guides/filters/block-filters.md#block-styles) for existing blocks. +Plugins and Themes can also register [custom block style](/docs/reference-guides/block-api/block-styles.md) for existing blocks. #### attributes (optional) diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index d2d66fc0d12de3..25fe5746a04122 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -428,8 +428,8 @@ Display a list of all pages. ([Source](https://github.com/WordPress/gutenberg/tr - **Name:** core/page-list - **Category:** widgets -- **Supports:** ~~html~~, ~~reusable~~ -- **Attributes:** parentPageID +- **Supports:** typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ +- **Attributes:** isNested, parentPageID ## Page List Item diff --git a/docs/reference-guides/data/README.md b/docs/reference-guides/data/README.md index 1134c1d5ddd307..5f4d8d92d4bd49 100644 --- a/docs/reference-guides/data/README.md +++ b/docs/reference-guides/data/README.md @@ -12,7 +12,6 @@ - [**core/editor**: The Post Editor’s Data](/docs/reference-guides/data/data-core-editor.md) - [**core/keyboard-shortcuts**: The Keyboard Shortcuts Data](/docs/reference-guides/data/data-core-keyboard-shortcuts.md) - [**core/notices**: Notices Data](/docs/reference-guides/data/data-core-notices.md) -- [**core/nux**: The NUX (New User Experience) Data](/docs/reference-guides/data/data-core-nux.md) - [**core/preferences**: Preferences](/docs/reference-guides/data/data-core-preferences.md) - [**core/reusable-blocks**: Reusable blocks](/docs/reference-guides/data/data-core-reusable-blocks.md) - [**core/rich-text**: Rich Text](/docs/reference-guides/data/data-core-rich-text.md) diff --git a/docs/reference-guides/data/data-core-nux.md b/docs/reference-guides/data/data-core-nux.md deleted file mode 100644 index 4d2e8a0d98d546..00000000000000 --- a/docs/reference-guides/data/data-core-nux.md +++ /dev/null @@ -1,99 +0,0 @@ -# The NUX (New User Experience) Data - -Namespace: `core/nux`. - -## Selectors - - - -### areTipsEnabled - -Returns whether or not tips are globally enabled. - -_Parameters_ - -- _state_ `Object`: Global application state. - -_Returns_ - -- `boolean`: Whether tips are globally enabled. - -### getAssociatedGuide - -Returns an object describing the guide, if any, that the given tip is a part -of. - -_Parameters_ - -- _state_ `Object`: Global application state. -- _tipId_ `string`: The tip to query. - -_Returns_ - -- `?NUXGuideInfo`: Information about the associated guide. - -### isTipVisible - -Determines whether or not the given tip is showing. Tips are hidden if they -are disabled, have been dismissed, or are not the current tip in any -guide that they have been added to. - -_Parameters_ - -- _state_ `Object`: Global application state. -- _tipId_ `string`: The tip to query. - -_Returns_ - -- `boolean`: Whether or not the given tip is showing. - - - -## Actions - - - -### disableTips - -Returns an action object that, when dispatched, prevents all tips from -showing again. - -_Returns_ - -- `Object`: Action object. - -### dismissTip - -Returns an action object that, when dispatched, dismisses the given tip. A -dismissed tip will not show again. - -_Parameters_ - -- _id_ `string`: The tip to dismiss. - -_Returns_ - -- `Object`: Action object. - -### enableTips - -Returns an action object that, when dispatched, makes all tips show again. - -_Returns_ - -- `Object`: Action object. - -### triggerGuide - -Returns an action object that, when dispatched, presents a guide that takes -the user through a series of tips step by step. - -_Parameters_ - -- _tipIds_ `string[]`: Which tips to show in the guide. - -_Returns_ - -- `Object`: Action object. - - diff --git a/docs/reference-guides/filters/block-filters.md b/docs/reference-guides/filters/block-filters.md index 823248bb451bdd..e41215d67065a1 100644 --- a/docs/reference-guides/filters/block-filters.md +++ b/docs/reference-guides/filters/block-filters.md @@ -179,19 +179,18 @@ _Example:_ ```js const { createHigherOrderComponent } = wp.compose; -const { Fragment } = wp.element; const { InspectorControls } = wp.blockEditor; const { PanelBody } = wp.components; const withInspectorControls = createHigherOrderComponent( ( BlockEdit ) => { return ( props ) => { return ( - + <> My custom control - + ); }; }, 'withInspectorControl' ); diff --git a/docs/reference-guides/slotfills/README.md b/docs/reference-guides/slotfills/README.md index b3ab4dcc083862..c41e6da0d8eb9f 100644 --- a/docs/reference-guides/slotfills/README.md +++ b/docs/reference-guides/slotfills/README.md @@ -75,7 +75,7 @@ const PostStatus = ( { isOpened, onTogglePanel } ) => ( > { ( fills ) => ( - + <> @@ -84,7 +84,7 @@ const PostStatus = ( { isOpened, onTogglePanel } ) => ( { fills } - + ) } diff --git a/docs/reference-guides/slotfills/plugin-sidebar-more-menu-item.md b/docs/reference-guides/slotfills/plugin-sidebar-more-menu-item.md index 8ede72e782b02c..4d32d754578cc4 100644 --- a/docs/reference-guides/slotfills/plugin-sidebar-more-menu-item.md +++ b/docs/reference-guides/slotfills/plugin-sidebar-more-menu-item.md @@ -9,17 +9,16 @@ This is done by setting the `target` on `` to match t import { registerPlugin } from '@wordpress/plugins'; import { PluginSidebar, PluginSidebarMoreMenuItem } from '@wordpress/edit-post'; import { image } from '@wordpress/icons'; -import { Fragment } from '@wordpress/element'; const PluginSidebarMoreMenuItemTest = () => ( - + <> Expanded Sidebar - More item Content of the sidebar - + ); registerPlugin( 'plugin-sidebar-expanded-test', { diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index a694cf63e909fa..24b060605da718 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -52,6 +52,16 @@ Settings related to borders. --- +### shadow + +Settings related to shadows. + +| Property | Type | Default | Props | +| --- | --- | --- |--- | +| palette | array | | name, shadow, slug | + +--- + ### color Settings related to colors. @@ -119,7 +129,7 @@ Settings related to typography. | customFontSize | boolean | true | | | fontStyle | boolean | true | | | fontWeight | boolean | true | | -| fluid | boolean | | | +| fluid | undefined | false | | | letterSpacing | boolean | true | | | lineHeight | boolean | false | | | textDecoration | boolean | true | | @@ -145,10 +155,10 @@ Border styles. | Property | Type | Props | | --- | --- |--- | -| color | string | | -| radius | undefined | | -| style | string | | -| width | string | | +| color | string, object | | +| radius | string, object | | +| style | string, object | | +| width | string, object | | | top | object | color, style, width | | right | object | color, style, width | | bottom | object | color, style, width | @@ -162,9 +172,9 @@ Color styles. | Property | Type | Props | | --- | --- |--- | -| background | string | | -| gradient | string | | -| text | string | | +| background | string, object | | +| gradient | string, object | | +| text | string, object | | --- @@ -174,7 +184,7 @@ Spacing styles. | Property | Type | Props | | --- | --- |--- | -| blockGap | string | | +| blockGap | string, object | | | margin | object | bottom, left, right, top | | padding | object | bottom, left, right, top | @@ -186,14 +196,14 @@ Typography styles. | Property | Type | Props | | --- | --- |--- | -| fontFamily | string | | -| fontSize | string | | -| fontStyle | string | | -| fontWeight | string | | -| letterSpacing | string | | -| lineHeight | string | | -| textDecoration | string | | -| textTransform | string | | +| fontFamily | string, object | | +| fontSize | string, object | | +| fontStyle | string, object | | +| fontWeight | string, object | | +| letterSpacing | string, object | | +| lineHeight | string, object | | +| textDecoration | string, object | | +| textTransform | string, object | | --- @@ -203,7 +213,7 @@ CSS and SVG filter styles. | Property | Type | Props | | --- | --- |--- | -| duotone | string | | +| duotone | string, object | | --- @@ -220,10 +230,10 @@ Outline styles. | Property | Type | Props | | --- | --- |--- | -| color | string | | -| offset | string | | -| style | string | | -| width | string | | +| color | string, object | | +| offset | string, object | | +| style | string, object | | +| width | string, object | | --- diff --git a/docs/toc.json b/docs/toc.json index 532e6ef2d20e1d..4203f40c16cbc4 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -278,7 +278,6 @@ "docs/reference-guides/data/data-core-keyboard-shortcuts.md": [] }, { "docs/reference-guides/data/data-core-notices.md": [] }, - { "docs/reference-guides/data/data-core-nux.md": [] }, { "docs/reference-guides/data/data-core-preferences.md": [] }, diff --git a/gutenberg.php b/gutenberg.php index 4d4489ef4c0615..af75dfb7463f20 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the new block editor in core. * Requires at least: 6.0 * Requires PHP: 5.6 - * Version: 14.8.0-rc.1 + * Version: 14.9.0-rc.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/block-supports/typography.php b/lib/block-supports/typography.php index 01d223b84281eb..aa6c74a5d2e73c 100644 --- a/lib/block-supports/typography.php +++ b/lib/block-supports/typography.php @@ -168,16 +168,21 @@ function gutenberg_apply_typography_support( $block_type, $block_attributes ) { } /** - * Note: this method is for backwards compatibility. - * It mostly replaces `gutenberg_typography_get_css_variable_inline_style()`. - * * Generates an inline style value for a typography feature e.g. text decoration, * text transform, and font style. * - * @param string $style_value A raw style value for a single typography feature from a block's style attribute. - * @param string $css_property Slug for the CSS property the inline style sets. + * Note: This function is for backwards compatibility. + * * It is necessary to parse older blocks whose typography styles contain presets. + * * It mostly replaces the deprecated `wp_typography_get_css_variable_inline_style()`, + * but skips compiling a CSS declaration as the style engine takes over this role. + * + * @link https://github.com/wordpress/gutenberg/pull/27555 + * + * @since 6.1.0 * - * @return string? A CSS inline style value. + * @param string $style_value A raw style value for a single typography feature from a block's style attribute. + * @param string $css_property Slug for the CSS property the inline style sets. + * @return string A CSS inline style value. */ function gutenberg_typography_get_preset_inline_style_value( $style_value, $css_property ) { // If the style value is not a preset CSS variable go no further. @@ -185,10 +190,12 @@ function gutenberg_typography_get_preset_inline_style_value( $style_value, $css_ return $style_value; } - // For backwards compatibility. - // Presets were removed in https://github.com/WordPress/gutenberg/pull/27555. - // We have a preset CSS variable as the style. - // Get the style value from the string and return CSS style. + /* + * For backwards compatibility. + * Presets were removed in WordPress/gutenberg#27555. + * We have a preset CSS variable as the style. + * Get the style value from the string and return CSS style. + */ $index_to_splice = strrpos( $style_value, '|' ) + 1; $slug = _wp_to_kebab_case( substr( $style_value, $index_to_splice ) ); @@ -197,14 +204,16 @@ function gutenberg_typography_get_preset_inline_style_value( $style_value, $css_ } /** - * Deprecated. - * This method is no longer used and will have to be deprecated in Core. + * This method is no longer used and has been deprecated in Core since 6.1.0. * - * It can be deleted once migrated to the next WordPress version. + * It can be deleted once Gutenberg's minimum supported WordPress version is >= 6.1 * * Generates an inline style for a typography feature e.g. text decoration, * text transform, and font style. * + * @since 5.8.0 + * @deprecated 6.1.0 + * * @param array $attributes Block's attributes. * @param string $feature Key for the feature within the typography styles. * @param string $css_property Slug for the CSS property the inline style sets. @@ -451,18 +460,25 @@ function gutenberg_get_typography_font_size_value( $preset, $should_use_fluid_ty // Checks if fluid font sizes are activated. $typography_settings = gutenberg_get_global_settings( array( 'typography' ) ); - $should_use_fluid_typography = isset( $typography_settings['fluid'] ) && true === $typography_settings['fluid'] ? true : $should_use_fluid_typography; + $should_use_fluid_typography + = isset( $typography_settings['fluid'] ) && + ( true === $typography_settings['fluid'] || is_array( $typography_settings['fluid'] ) ) ? + true : + $should_use_fluid_typography; if ( ! $should_use_fluid_typography ) { return $preset['size']; } + $fluid_settings = isset( $typography_settings['fluid'] ) && is_array( $typography_settings['fluid'] ) ? $typography_settings['fluid'] : array(); + // Defaults. $default_maximum_viewport_width = '1600px'; $default_minimum_viewport_width = '768px'; $default_minimum_font_size_factor = 0.75; $default_scale_factor = 1; - $default_minimum_font_size_limit = '14px'; + $has_min_font_size = isset( $fluid_settings['minFontSize'] ) && ! empty( gutenberg_get_typography_value_and_unit( $fluid_settings['minFontSize'] ) ); + $default_minimum_font_size_limit = $has_min_font_size ? $fluid_settings['minFontSize'] : '14px'; // Font sizes. $fluid_font_size_settings = isset( $preset['fluid'] ) ? $preset['fluid'] : null; diff --git a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php b/lib/class-wp-theme-json-gutenberg.php similarity index 52% rename from lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php rename to lib/class-wp-theme-json-gutenberg.php index 0c0ec0451aec4e..ddad36e15fc02d 100644 --- a/lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -1,8 +1,9 @@ array( ':visited', ':hover', ':focus', ':active' ), - 'button' => array( ':visited', ':hover', ':focus', ':active' ), - ); + const ROOT_BLOCK_SELECTOR = 'body'; /** * The sources of data this object can represent. * + * @since 5.8.0 + * @since 6.1.0 Added 'blocks'. * @var string[] */ const VALID_ORIGINS = array( @@ -44,11 +59,149 @@ class WP_Theme_JSON_6_1 extends WP_Theme_JSON { 'custom', ); + /** + * Presets are a set of values that serve + * to bootstrap some styles: colors, font sizes, etc. + * + * They are a unkeyed array of values such as: + * + * ```php + * array( + * array( + * 'slug' => 'unique-name-within-the-set', + * 'name' => 'Name for the UI', + * => 'value' + * ), + * ) + * ``` + * + * This contains the necessary metadata to process them: + * + * - path => Where to find the preset within the settings section. + * - prevent_override => Disables override of default presets by theme presets. + * The relationship between whether to override the defaults + * and whether the defaults are enabled is inverse: + * - If defaults are enabled => theme presets should not be overriden + * - If defaults are disabled => theme presets should be overriden + * For example, a theme sets defaultPalette to false, + * making the default palette hidden from the user. + * In that case, we want all the theme presets to be present, + * so they should override the defaults by setting this false. + * - use_default_names => whether to use the default names + * - value_key => the key that represents the value + * - value_func => optionally, instead of value_key, a function to generate + * the value that takes a preset as an argument + * (either value_key or value_func should be present) + * - css_vars => template string to use in generating the CSS Custom Property. + * Example output: "--wp--preset--duotone--blue: " will generate as many CSS Custom Properties as presets defined + * substituting the $slug for the slug's value for each preset value. + * - classes => array containing a structure with the classes to + * generate for the presets, where for each array item + * the key is the class name and the value the property name. + * The "$slug" substring will be replaced by the slug of each preset. + * For example: + * 'classes' => array( + * '.has-$slug-color' => 'color', + * '.has-$slug-background-color' => 'background-color', + * '.has-$slug-border-color' => 'border-color', + * ) + * - properties => array of CSS properties to be used by kses to + * validate the content of each preset + * by means of the remove_insecure_properties method. + * + * @since 5.8.0 + * @since 5.9.0 Added the `color.duotone` and `typography.fontFamilies` presets, + * `use_default_names` preset key, and simplified the metadata structure. + * @since 6.0.0 Replaced `override` with `prevent_override` and updated the + * `prevent_override` value for `color.duotone` to use `color.defaultDuotone`. + * @var array + */ + const PRESETS_METADATA = array( + array( + 'path' => array( 'shadow', 'palette' ), + 'prevent_override' => array( 'shadow', 'defaultPalette' ), + 'use_default_names' => false, + 'value_key' => 'shadow', + 'css_vars' => '--wp--preset--shadow--$slug', + 'classes' => array(), + 'properties' => array( 'box-shadow' ), + ), + array( + 'path' => array( 'color', 'palette' ), + 'prevent_override' => array( 'color', 'defaultPalette' ), + 'use_default_names' => false, + 'value_key' => 'color', + 'css_vars' => '--wp--preset--color--$slug', + 'classes' => array( + '.has-$slug-color' => 'color', + '.has-$slug-background-color' => 'background-color', + '.has-$slug-border-color' => 'border-color', + ), + 'properties' => array( 'color', 'background-color', 'border-color' ), + ), + array( + 'path' => array( 'color', 'gradients' ), + 'prevent_override' => array( 'color', 'defaultGradients' ), + 'use_default_names' => false, + 'value_key' => 'gradient', + 'css_vars' => '--wp--preset--gradient--$slug', + 'classes' => array( '.has-$slug-gradient-background' => 'background' ), + 'properties' => array( 'background' ), + ), + array( + 'path' => array( 'color', 'duotone' ), + 'prevent_override' => array( 'color', 'defaultDuotone' ), + 'use_default_names' => false, + 'value_func' => 'gutenberg_get_duotone_filter_property', + 'css_vars' => '--wp--preset--duotone--$slug', + 'classes' => array(), + 'properties' => array( 'filter' ), + ), + array( + 'path' => array( 'typography', 'fontSizes' ), + 'prevent_override' => false, + 'use_default_names' => true, + 'value_func' => 'gutenberg_get_typography_font_size_value', + 'css_vars' => '--wp--preset--font-size--$slug', + 'classes' => array( '.has-$slug-font-size' => 'font-size' ), + 'properties' => array( 'font-size' ), + ), + array( + 'path' => array( 'typography', 'fontFamilies' ), + 'prevent_override' => false, + 'use_default_names' => false, + 'value_key' => 'fontFamily', + 'css_vars' => '--wp--preset--font-family--$slug', + 'classes' => array( '.has-$slug-font-family' => 'font-family' ), + 'properties' => array( 'font-family' ), + ), + array( + 'path' => array( 'spacing', 'spacingSizes' ), + 'prevent_override' => false, + 'use_default_names' => true, + 'value_key' => 'size', + 'css_vars' => '--wp--preset--spacing--$slug', + 'classes' => array(), + 'properties' => array( 'padding', 'margin' ), + ), + ); + /** * Metadata for style properties. * * Each element is a direct mapping from the CSS property name to the * path to the value in theme.json & block attributes. + * + * @since 5.8.0 + * @since 5.9.0 Added the `border-*`, `font-family`, `font-style`, `font-weight`, + * `letter-spacing`, `margin-*`, `padding-*`, `--wp--style--block-gap`, + * `text-decoration`, `text-transform`, and `filter` properties, + * simplified the metadata structure. + * @since 6.1.0 Added the `border-*-color`, `border-*-width`, `border-*-style`, + * `--wp--style--root--padding-*`, and `box-shadow` properties, + * removed the `--wp--style--block-gap` property. + * @since 6.2.0 Added `min-height`. + * @var array */ const PROPERTIES_METADATA = array( 'background' => array( 'color', 'gradient' ), @@ -85,6 +238,7 @@ class WP_Theme_JSON_6_1 extends WP_Theme_JSON { 'margin-right' => array( 'spacing', 'margin', 'right' ), 'margin-bottom' => array( 'spacing', 'margin', 'bottom' ), 'margin-left' => array( 'spacing', 'margin', 'left' ), + 'min-height' => array( 'dimensions', 'minHeight' ), 'outline-color' => array( 'outline', 'color' ), 'outline-offset' => array( 'outline', 'offset' ), 'outline-style' => array( 'outline', 'style' ), @@ -106,260 +260,150 @@ class WP_Theme_JSON_6_1 extends WP_Theme_JSON { ); /** - * The valid elements that can be found under styles. + * Protected style properties. * - * @var string[] + * These style properties are only rendered if a setting enables it + * via a value other than `null`. + * + * Each element maps the style property to the corresponding theme.json + * setting key. + * + * @since 5.9.0 */ - const ELEMENTS = array( - 'link' => 'a:where(:not(.wp-element-button))', // The where is needed to lower the specificity. - 'heading' => 'h1, h2, h3, h4, h5, h6', - 'h1' => 'h1', - 'h2' => 'h2', - 'h3' => 'h3', - 'h4' => 'h4', - 'h5' => 'h5', - 'h6' => 'h6', - 'button' => '.wp-element-button, .wp-block-button__link', // We have the .wp-block-button__link class so that this will target older buttons that have been serialized. - 'caption' => '.wp-element-caption, .wp-block-audio figcaption, .wp-block-embed figcaption, .wp-block-gallery figcaption, .wp-block-image figcaption, .wp-block-table figcaption, .wp-block-video figcaption', // The block classes are necessary to target older content that won't use the new class names. - 'cite' => 'cite', - ); - - const __EXPERIMENTAL_ELEMENT_CLASS_NAMES = array( - 'button' => 'wp-element-button', - 'caption' => 'wp-element-caption', - ); - - // List of block support features that can have their related styles - // generated under their own feature level selector rather than the block's. - const BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS = array( - '__experimentalBorder' => 'border', - 'color' => 'color', - 'spacing' => 'spacing', - 'typography' => 'typography', + const PROTECTED_PROPERTIES = array( + 'spacing.blockGap' => array( 'spacing', 'blockGap' ), ); /** - * Constructor. + * Indirect metadata for style properties that are not directly output. * - * @since 5.8.0 + * Each element is a direct mapping from a CSS property name to the + * path to the value in theme.json & block attributes. * - * @param array $theme_json A structure that follows the theme.json schema. - * @param string $origin Optional. What source of data this object represents. - * One of 'default', 'theme', or 'custom'. Default 'theme'. + * Indirect properties are not output directly by `compute_style_properties`, + * but are used elsewhere in the processing of global styles. The indirect + * property is used to validate whether or not a style value is allowed. + * + * @since 6.2.0 + * @var array */ - public function __construct( $theme_json = array(), $origin = 'theme' ) { - if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) { - $origin = 'theme'; - } - - $this->theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); - $registry = WP_Block_Type_Registry::get_instance(); - $valid_block_names = array_keys( $registry->get_all_registered() ); - $valid_element_names = array_keys( static::ELEMENTS ); - $theme_json = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names ); - $this->theme_json = static::maybe_opt_in_into_settings( $theme_json ); - - // Internally, presets are keyed by origin. - $nodes = static::get_setting_nodes( $this->theme_json ); - foreach ( $nodes as $node ) { - foreach ( static::PRESETS_METADATA as $preset_metadata ) { - $path = array_merge( $node['path'], $preset_metadata['path'] ); - $preset = _wp_array_get( $this->theme_json, $path, null ); - if ( null !== $preset ) { - // If the preset is not already keyed by origin. - if ( isset( $preset[0] ) || empty( $preset ) ) { - _wp_array_set( $this->theme_json, $path, array( $origin => $preset ) ); - } - } - } - } - } + const INDIRECT_PROPERTIES_METADATA = array( + 'gap' => array( + array( 'spacing', 'blockGap' ), + ), + 'column-gap' => array( + array( 'spacing', 'blockGap', 'left' ), + ), + 'row-gap' => array( + array( 'spacing', 'blockGap', 'top' ), + ), + 'max-width' => array( + array( 'layout', 'contentSize' ), + array( 'layout', 'wideSize' ), + ), + ); /** - * Given an element name, returns a class name. + * The top-level keys a theme.json can have. * - * @param string $element The name of the element. - * - * @return string The name of the class. - * - * @since 6.1.0 + * @since 5.8.0 As `ALLOWED_TOP_LEVEL_KEYS`. + * @since 5.9.0 Renamed from `ALLOWED_TOP_LEVEL_KEYS` to `VALID_TOP_LEVEL_KEYS`, + * added the `customTemplates` and `templateParts` values. + * @var string[] */ - public static function get_element_class_name( $element ) { - return array_key_exists( $element, static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES ) ? static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ] : ''; - } + const VALID_TOP_LEVEL_KEYS = array( + 'customTemplates', + 'patterns', + 'settings', + 'styles', + 'templateParts', + 'version', + 'title', + ); /** - * Sanitizes the input according to the schemas. - * - * @since 5.8.0 - * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters. + * The valid properties under the settings key. * - * @param array $input Structure to sanitize. - * @param array $valid_block_names List of valid block names. - * @param array $valid_element_names List of valid element names. - * @return array The sanitized output. + * @since 5.8.0 As `ALLOWED_SETTINGS`. + * @since 5.9.0 Renamed from `ALLOWED_SETTINGS` to `VALID_SETTINGS`, + * added new properties for `border`, `color`, `spacing`, + * and `typography`, and renamed others according to the new schema. + * @since 6.0.0 Added `color.defaultDuotone`. + * @since 6.1.0 Added `layout.definitions` and `useRootPaddingAwareAlignments`. + * @since 6.2.0 Added `dimensions.minHeight`. + * @var array */ - protected static function sanitize( $input, $valid_block_names, $valid_element_names ) { - - $output = array(); - - if ( ! is_array( $input ) ) { - return $output; - } - - // Preserve only the top most level keys. - $output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) ); - - // Remove any rules that are annotated as "top" in VALID_STYLES constant. - // Some styles are only meant to be available at the top-level (e.g.: blockGap), - // hence, the schema for blocks & elements should not have them. - $styles_non_top_level = static::VALID_STYLES; - foreach ( array_keys( $styles_non_top_level ) as $section ) { - if ( array_key_exists( $section, $styles_non_top_level ) && is_array( $styles_non_top_level[ $section ] ) ) { - foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) { - if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) { - unset( $styles_non_top_level[ $section ][ $prop ] ); - } - } - } - } - - // Build the schema based on valid block & element names. - $schema = array(); - $schema_styles_elements = array(); - - // Set allowed element pseudo selectors based on per element allow list. - // Target data structure in schema: - // e.g. - // - top level elements: `$schema['styles']['elements']['link'][':hover']`. - // - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`. - foreach ( $valid_element_names as $element ) { - $schema_styles_elements[ $element ] = $styles_non_top_level; - - if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { - foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { - $schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level; - } - } - } - - $schema_styles_blocks = array(); - $schema_settings_blocks = array(); - foreach ( $valid_block_names as $block ) { - $schema_settings_blocks[ $block ] = static::VALID_SETTINGS; - $schema_styles_blocks[ $block ] = $styles_non_top_level; - $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; - } - - $schema['styles'] = static::VALID_STYLES; - $schema['styles']['blocks'] = $schema_styles_blocks; - $schema['styles']['elements'] = $schema_styles_elements; - $schema['settings'] = static::VALID_SETTINGS; - $schema['settings']['blocks'] = $schema_settings_blocks; - - // Remove anything that's not present in the schema. - foreach ( array( 'styles', 'settings' ) as $subtree ) { - if ( ! isset( $input[ $subtree ] ) ) { - continue; - } - - if ( ! is_array( $input[ $subtree ] ) ) { - unset( $output[ $subtree ] ); - continue; - } - - $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] ); - - if ( empty( $result ) ) { - unset( $output[ $subtree ] ); - } else { - $output[ $subtree ] = $result; - } - } - - return $output; - } - - /** - * Removes insecure data from theme.json. - * - * @since 5.9.0 - * - * @param array $theme_json Structure to sanitize. - * @return array Sanitized structure. - */ - public static function remove_insecure_properties( $theme_json ) { - $sanitized = array(); - - $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); - - $valid_block_names = array_keys( static::get_blocks_metadata() ); - $valid_element_names = array_keys( static::ELEMENTS ); - - $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names ); - - $blocks_metadata = static::get_blocks_metadata(); - $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata ); - - foreach ( $style_nodes as $metadata ) { - $input = _wp_array_get( $theme_json, $metadata['path'], array() ); - if ( empty( $input ) ) { - continue; - } - - $output = static::remove_insecure_styles( $input ); - - // Get a reference to element name from path. - // $metadata['path'] = array('styles','elements','link');. - $current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ]; - - // $output is stripped of pseudo selectors. Readd and process them - // for insecure styles here. - if ( array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { - - foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) { - if ( isset( $input[ $pseudo_selector ] ) ) { - $output[ $pseudo_selector ] = static::remove_insecure_styles( $input[ $pseudo_selector ] ); - } - } - } - - if ( ! empty( $output ) ) { - _wp_array_set( $sanitized, $metadata['path'], $output ); - } - } - - $setting_nodes = static::get_setting_nodes( $theme_json ); - foreach ( $setting_nodes as $metadata ) { - $input = _wp_array_get( $theme_json, $metadata['path'], array() ); - if ( empty( $input ) ) { - continue; - } - - $output = static::remove_insecure_settings( $input ); - if ( ! empty( $output ) ) { - _wp_array_set( $sanitized, $metadata['path'], $output ); - } - } - - if ( empty( $sanitized['styles'] ) ) { - unset( $theme_json['styles'] ); - } else { - $theme_json['styles'] = $sanitized['styles']; - } - - if ( empty( $sanitized['settings'] ) ) { - unset( $theme_json['settings'] ); - } else { - $theme_json['settings'] = $sanitized['settings']; - } - - return $theme_json; - } + const VALID_SETTINGS = array( + 'appearanceTools' => null, + 'useRootPaddingAwareAlignments' => null, + 'border' => array( + 'color' => null, + 'radius' => null, + 'style' => null, + 'width' => null, + ), + 'shadow' => array( + 'palette' => null, + 'defaultPalette' => null, + ), + 'color' => array( + 'background' => null, + 'custom' => null, + 'customDuotone' => null, + 'customGradient' => null, + 'defaultDuotone' => null, + 'defaultGradients' => null, + 'defaultPalette' => null, + 'duotone' => null, + 'gradients' => null, + 'link' => null, + 'palette' => null, + 'text' => null, + ), + 'custom' => null, + 'dimensions' => array( + 'minHeight' => null, + ), + 'layout' => array( + 'contentSize' => null, + 'definitions' => null, + 'wideSize' => null, + ), + 'spacing' => array( + 'customSpacingSize' => null, + 'spacingSizes' => null, + 'spacingScale' => null, + 'blockGap' => null, + 'margin' => null, + 'padding' => null, + 'units' => null, + ), + 'typography' => array( + 'fluid' => null, + 'customFontSize' => null, + 'dropCap' => null, + 'fontFamilies' => null, + 'fontSizes' => null, + 'fontStyle' => null, + 'fontWeight' => null, + 'letterSpacing' => null, + 'lineHeight' => null, + 'textDecoration' => null, + 'textTransform' => null, + ), + ); /** * The valid properties under the styles key. * + * @since 5.8.0 As `ALLOWED_STYLES`. + * @since 5.9.0 Renamed from `ALLOWED_STYLES` to `VALID_STYLES`, + * added new properties for `border`, `filter`, `spacing`, + * and `typography`. + * @since 6.1.0 Added new side properties for `border`, + * added new property `shadow`, + * updated `blockGap` to be allowed at any level. + * @since 6.2.0 Added `dimensions.minHeight`. * @var array */ const VALID_STYLES = array( @@ -378,7 +422,10 @@ public static function remove_insecure_properties( $theme_json ) { 'gradient' => null, 'text' => null, ), - 'shadow' => null, + 'css' => null, + 'dimensions' => array( + 'minHeight' => null, + ), 'filter' => array( 'duotone' => null, ), @@ -388,6 +435,7 @@ public static function remove_insecure_properties( $theme_json ) { 'style' => null, 'width' => null, ), + 'shadow' => null, 'spacing' => array( 'margin' => null, 'padding' => null, @@ -406,45 +454,338 @@ public static function remove_insecure_properties( $theme_json ) { ); /** - * Function that appends a sub-selector to a existing one. + * Defines which pseudo selectors are enabled for which elements. * - * Given the compounded $selector "h1, h2, h3" - * and the $to_append selector ".some-class" the result will be - * "h1.some-class, h2.some-class, h3.some-class". + * The order of the selectors should be: visited, hover, focus, active. + * This is to ensure that 'visited' has the lowest specificity + * and the other selectors can always overwrite it. + * + * See https://core.trac.wordpress.org/ticket/56928. + * Note: this will affect both top-level and block-level elements. + * + * @since 6.1.0 + */ + const VALID_ELEMENT_PSEUDO_SELECTORS = array( + 'link' => array( ':visited', ':hover', ':focus', ':active' ), + 'button' => array( ':visited', ':hover', ':focus', ':active' ), + ); + + /** + * The valid elements that can be found under styles. * * @since 5.8.0 - * @since 6.1.0 Added append position. + * @since 6.1.0 Added `heading`, `button`, and `caption` elements. + * @var string[] + */ + const ELEMENTS = array( + 'link' => 'a:where(:not(.wp-element-button))', // The `where` is needed to lower the specificity. + 'heading' => 'h1, h2, h3, h4, h5, h6', + 'h1' => 'h1', + 'h2' => 'h2', + 'h3' => 'h3', + 'h4' => 'h4', + 'h5' => 'h5', + 'h6' => 'h6', + // We have the .wp-block-button__link class so that this will target older buttons that have been serialized. + 'button' => '.wp-element-button, .wp-block-button__link', + // The block classes are necessary to target older content that won't use the new class names. + 'caption' => '.wp-element-caption, .wp-block-audio figcaption, .wp-block-embed figcaption, .wp-block-gallery figcaption, .wp-block-image figcaption, .wp-block-table figcaption, .wp-block-video figcaption', + 'cite' => 'cite', + ); + + const __EXPERIMENTAL_ELEMENT_CLASS_NAMES = array( + 'button' => 'wp-element-button', + 'caption' => 'wp-element-caption', + ); + + /** + * List of block support features that can have their related styles + * generated under their own feature level selector rather than the block's. * - * @param string $selector Original selector. - * @param string $to_append Selector to append. - * @param string $position A position sub-selector should be appended. Default: 'right'. - * @return string + * @since 6.1.0 + * @var string[] */ - protected static function append_to_selector( $selector, $to_append, $position = 'right' ) { - $new_selectors = array(); - $selectors = explode( ',', $selector ); - foreach ( $selectors as $sel ) { - $new_selectors[] = 'right' === $position ? $sel . $to_append : $to_append . $sel; + const BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS = array( + '__experimentalBorder' => 'border', + 'color' => 'color', + 'spacing' => 'spacing', + 'typography' => 'typography', + ); + + /** + * Returns a class name by an element name. + * + * @since 6.1.0 + * + * @param string $element The name of the element. + * @return string The name of the class. + */ + public static function get_element_class_name( $element ) { + $class_name = ''; + + // TODO: Replace array_key_exists() with isset() check once WordPress drops + // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. + if ( array_key_exists( $element, static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES ) ) { + $class_name = static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ]; } - return implode( ',', $new_selectors ); + return $class_name; } /** - * Returns the metadata for each block. + * Options that settings.appearanceTools enables. * - * Example: + * @since 6.0.0 + * @var array + */ + const APPEARANCE_TOOLS_OPT_INS = array( + array( 'border', 'color' ), + array( 'border', 'radius' ), + array( 'border', 'style' ), + array( 'border', 'width' ), + array( 'color', 'link' ), + array( 'dimensions', 'minHeight' ), + array( 'spacing', 'blockGap' ), + array( 'spacing', 'margin' ), + array( 'spacing', 'padding' ), + array( 'typography', 'lineHeight' ), + ); + + /** + * The latest version of the schema in use. * - * { - * 'core/paragraph': { - * 'selector': 'p', - * 'elements': { - * 'link' => 'link selector', - * 'etc' => 'element selector' - * } - * }, - * 'core/heading': { - * 'selector': 'h1', + * @since 5.8.0 + * @since 5.9.0 Changed value from 1 to 2. + * @var int + */ + const LATEST_SCHEMA = 2; + + /** + * Constructor. + * + * @since 5.8.0 + * + * @param array $theme_json A structure that follows the theme.json schema. + * @param string $origin Optional. What source of data this object represents. + * One of 'default', 'theme', or 'custom'. Default 'theme'. + */ + public function __construct( $theme_json = array(), $origin = 'theme' ) { + if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) { + $origin = 'theme'; + } + + $this->theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); + $registry = WP_Block_Type_Registry::get_instance(); + $valid_block_names = array_keys( $registry->get_all_registered() ); + $valid_element_names = array_keys( static::ELEMENTS ); + $theme_json = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names ); + $this->theme_json = static::maybe_opt_in_into_settings( $theme_json ); + + // Internally, presets are keyed by origin. + $nodes = static::get_setting_nodes( $this->theme_json ); + foreach ( $nodes as $node ) { + foreach ( static::PRESETS_METADATA as $preset_metadata ) { + $path = $node['path']; + foreach ( $preset_metadata['path'] as $subpath ) { + $path[] = $subpath; + } + $preset = _wp_array_get( $this->theme_json, $path, null ); + if ( null !== $preset ) { + // If the preset is not already keyed by origin. + if ( isset( $preset[0] ) || empty( $preset ) ) { + _wp_array_set( $this->theme_json, $path, array( $origin => $preset ) ); + } + } + } + } + } + + /** + * Enables some opt-in settings if theme declared support. + * + * @since 5.9.0 + * + * @param array $theme_json A theme.json structure to modify. + * @return array The modified theme.json structure. + */ + protected static function maybe_opt_in_into_settings( $theme_json ) { + $new_theme_json = $theme_json; + + if ( + isset( $new_theme_json['settings']['appearanceTools'] ) && + true === $new_theme_json['settings']['appearanceTools'] + ) { + static::do_opt_in_into_settings( $new_theme_json['settings'] ); + } + + if ( isset( $new_theme_json['settings']['blocks'] ) && is_array( $new_theme_json['settings']['blocks'] ) ) { + foreach ( $new_theme_json['settings']['blocks'] as &$block ) { + if ( isset( $block['appearanceTools'] ) && ( true === $block['appearanceTools'] ) ) { + static::do_opt_in_into_settings( $block ); + } + } + } + + return $new_theme_json; + } + + /** + * Enables some settings. + * + * @since 5.9.0 + * + * @param array $context The context to which the settings belong. + */ + protected static function do_opt_in_into_settings( &$context ) { + foreach ( static::APPEARANCE_TOOLS_OPT_INS as $path ) { + // Use "unset prop" as a marker instead of "null" because + // "null" can be a valid value for some props (e.g. blockGap). + if ( 'unset prop' === _wp_array_get( $context, $path, 'unset prop' ) ) { + _wp_array_set( $context, $path, true ); + } + } + + unset( $context['appearanceTools'] ); + } + + /** + * Sanitizes the input according to the schemas. + * + * @since 5.8.0 + * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters. + * + * @param array $input Structure to sanitize. + * @param array $valid_block_names List of valid block names. + * @param array $valid_element_names List of valid element names. + * @return array The sanitized output. + */ + protected static function sanitize( $input, $valid_block_names, $valid_element_names ) { + + $output = array(); + + if ( ! is_array( $input ) ) { + return $output; + } + + // Preserve only the top most level keys. + $output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) ); + + /* + * Remove any rules that are annotated as "top" in VALID_STYLES constant. + * Some styles are only meant to be available at the top-level (e.g.: blockGap), + * hence, the schema for blocks & elements should not have them. + */ + $styles_non_top_level = static::VALID_STYLES; + foreach ( array_keys( $styles_non_top_level ) as $section ) { + // array_key_exists() needs to be used instead of isset() because the value can be null. + if ( array_key_exists( $section, $styles_non_top_level ) && is_array( $styles_non_top_level[ $section ] ) ) { + foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) { + if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) { + unset( $styles_non_top_level[ $section ][ $prop ] ); + } + } + } + } + + // Build the schema based on valid block & element names. + $schema = array(); + $schema_styles_elements = array(); + + /* + * Set allowed element pseudo selectors based on per element allow list. + * Target data structure in schema: + * e.g. + * - top level elements: `$schema['styles']['elements']['link'][':hover']`. + * - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`. + */ + foreach ( $valid_element_names as $element ) { + $schema_styles_elements[ $element ] = $styles_non_top_level; + + // TODO: Replace array_key_exists() with isset() check once WordPress drops + // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. + if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { + $schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level; + } + } + } + + $schema_styles_blocks = array(); + $schema_settings_blocks = array(); + foreach ( $valid_block_names as $block ) { + $schema_settings_blocks[ $block ] = static::VALID_SETTINGS; + $schema_styles_blocks[ $block ] = $styles_non_top_level; + $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; + } + + $schema['styles'] = static::VALID_STYLES; + $schema['styles']['blocks'] = $schema_styles_blocks; + $schema['styles']['elements'] = $schema_styles_elements; + $schema['settings'] = static::VALID_SETTINGS; + $schema['settings']['blocks'] = $schema_settings_blocks; + + // Remove anything that's not present in the schema. + foreach ( array( 'styles', 'settings' ) as $subtree ) { + if ( ! isset( $input[ $subtree ] ) ) { + continue; + } + + if ( ! is_array( $input[ $subtree ] ) ) { + unset( $output[ $subtree ] ); + continue; + } + + $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] ); + + if ( empty( $result ) ) { + unset( $output[ $subtree ] ); + } else { + $output[ $subtree ] = $result; + } + } + + return $output; + } + + /** + * Appends a sub-selector to an existing one. + * + * Given the compounded $selector "h1, h2, h3" + * and the $to_append selector ".some-class" the result will be + * "h1.some-class, h2.some-class, h3.some-class". + * + * @since 5.8.0 + * @since 6.1.0 Added append position. + * + * @param string $selector Original selector. + * @param string $to_append Selector to append. + * @param string $position A position sub-selector should be appended. Default 'right'. + * @return string The new selector. + */ + protected static function append_to_selector( $selector, $to_append, $position = 'right' ) { + $new_selectors = array(); + $selectors = explode( ',', $selector ); + foreach ( $selectors as $sel ) { + $new_selectors[] = 'right' === $position ? $sel . $to_append : $to_append . $sel; + } + return implode( ',', $new_selectors ); + } + + /** + * Returns the metadata for each block. + * + * Example: + * + * { + * 'core/paragraph': { + * 'selector': 'p', + * 'elements': { + * 'link' => 'link selector', + * 'etc' => 'element selector' + * } + * }, + * 'core/heading': { + * 'selector': 'h1', * 'elements': {} * }, * 'core/image': { @@ -454,17 +795,23 @@ protected static function append_to_selector( $selector, $to_append, $position = * } * } * + * @since 5.8.0 + * @since 5.9.0 Added `duotone` key with CSS selector. + * @since 6.1.0 Added `features` key with block support feature level selectors. + * * @return array Block metadata. */ protected static function get_blocks_metadata() { - if ( null !== static::$blocks_metadata ) { + // NOTE: the compat/6.1 version of this method in Gutenberg did not have these changes. + $registry = WP_Block_Type_Registry::get_instance(); + $blocks = $registry->get_all_registered(); + + // Is there metadata for all currently registered blocks? + $blocks = array_diff_key( $blocks, static::$blocks_metadata ); + if ( empty( $blocks ) ) { return static::$blocks_metadata; } - static::$blocks_metadata = array(); - - $registry = WP_Block_Type_Registry::get_instance(); - $blocks = $registry->get_all_registered(); foreach ( $blocks as $block_name => $block_type ) { if ( isset( $block_type->supports['__experimentalSelector'] ) && @@ -501,7 +848,7 @@ protected static function get_blocks_metadata() { static::$blocks_metadata[ $block_name ]['features'] = $features; } - // Assign defaults, then override those that the block sets by itself. + // Assign defaults, then overwrite those that the block sets by itself. // If the block selector is compounded, will append the element to each // individual block selector. $block_selectors = explode( ',', static::$blocks_metadata[ $block_name ]['selector'] ); @@ -517,174 +864,90 @@ protected static function get_blocks_metadata() { static::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector ); } } + return static::$blocks_metadata; } /** - * Builds metadata for the style nodes, which returns in the form of: + * Given a tree, removes the keys that are not present in the schema. * - * [ - * [ - * 'path' => [ 'path', 'to', 'some', 'node' ], - * 'selector' => 'CSS selector for some node', - * 'duotone' => 'CSS selector for duotone for some node' - * ], - * [ - * 'path' => ['path', 'to', 'other', 'node' ], - * 'selector' => 'CSS selector for other node', - * 'duotone' => null - * ], - * ] + * It is recursive and modifies the input in-place. * * @since 5.8.0 * - * @param array $theme_json The tree to extract style nodes from. - * @param array $selectors List of selectors per block. - * @return array + * @param array $tree Input to process. + * @param array $schema Schema to adhere to. + * @return array The modified $tree. */ - protected static function get_style_nodes( $theme_json, $selectors = array() ) { - $nodes = array(); - if ( ! isset( $theme_json['styles'] ) ) { - return $nodes; - } - - // Top-level. - $nodes[] = array( - 'path' => array( 'styles' ), - 'selector' => static::ROOT_BLOCK_SELECTOR, - ); + protected static function remove_keys_not_in_schema( $tree, $schema ) { + $tree = array_intersect_key( $tree, $schema ); - if ( isset( $theme_json['styles']['elements'] ) ) { - foreach ( self::ELEMENTS as $element => $selector ) { - if ( ! isset( $theme_json['styles']['elements'][ $element ] ) || ! array_key_exists( $element, static::ELEMENTS ) ) { - continue; - } - - // Handle element defaults. - $nodes[] = array( - 'path' => array( 'styles', 'elements', $element ), - 'selector' => static::ELEMENTS[ $element ], - ); - - // Handle any pseudo selectors for the element. - if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { - foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { + foreach ( $schema as $key => $data ) { + if ( ! isset( $tree[ $key ] ) ) { + continue; + } - if ( isset( $theme_json['styles']['elements'][ $element ][ $pseudo_selector ] ) ) { + if ( is_array( $schema[ $key ] ) && is_array( $tree[ $key ] ) ) { + $tree[ $key ] = static::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] ); - $nodes[] = array( - 'path' => array( 'styles', 'elements', $element ), - 'selector' => static::append_to_selector( static::ELEMENTS[ $element ], $pseudo_selector ), - ); - } - } + if ( empty( $tree[ $key ] ) ) { + unset( $tree[ $key ] ); } + } elseif ( is_array( $schema[ $key ] ) && ! is_array( $tree[ $key ] ) ) { + unset( $tree[ $key ] ); } } - // Blocks. - if ( ! isset( $theme_json['styles']['blocks'] ) ) { - return $nodes; - } - - $nodes = array_merge( $nodes, static::get_block_nodes( $theme_json, $selectors ) ); - - // This filter allows us to modify the output of WP_Theme_JSON so that we can do things like loading block CSS independently. - return apply_filters( 'wp_theme_json_get_style_nodes', $nodes ); + return $tree; } /** - * A public helper to get the block nodes from a theme.json file. + * Returns the existing settings for each block. * - * @return array The block nodes in theme.json. - */ - public function get_styles_block_nodes() { - return static::get_block_nodes( $this->theme_json ); - } - - /** - * An internal method to get the block nodes from a theme.json file. + * Example: * - * @param array $theme_json The theme.json converted to an array. - * @param array $selectors Optional list of selectors per block. + * { + * 'root': { + * 'color': { + * 'custom': true + * } + * }, + * 'core/paragraph': { + * 'spacing': { + * 'customPadding': true + * } + * } + * } * - * @return array The block nodes in theme.json. + * @since 5.8.0 + * + * @return array Settings per block. */ - private static function get_block_nodes( $theme_json, $selectors = array() ) { - $selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors; - $nodes = array(); - if ( ! isset( $theme_json['styles'] ) ) { - return $nodes; - } - - // Blocks. - if ( ! isset( $theme_json['styles']['blocks'] ) ) { - return $nodes; - } - - foreach ( $theme_json['styles']['blocks'] as $name => $node ) { - $selector = null; - if ( isset( $selectors[ $name ]['selector'] ) ) { - $selector = $selectors[ $name ]['selector']; - } - - $duotone_selector = null; - if ( isset( $selectors[ $name ]['duotone'] ) ) { - $duotone_selector = $selectors[ $name ]['duotone']; - } - - $feature_selectors = null; - if ( isset( $selectors[ $name ]['features'] ) ) { - $feature_selectors = $selectors[ $name ]['features']; - } - - $nodes[] = array( - 'name' => $name, - 'path' => array( 'styles', 'blocks', $name ), - 'selector' => $selector, - 'duotone' => $duotone_selector, - 'features' => $feature_selectors, - ); - - if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) { - foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) { - $nodes[] = array( - 'path' => array( 'styles', 'blocks', $name, 'elements', $element ), - 'selector' => $selectors[ $name ]['elements'][ $element ], - ); - - // Handle any pseudo selectors for the element. - if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { - foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { - if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) { - - $nodes[] = array( - 'path' => array( 'styles', 'blocks', $name, 'elements', $element ), - 'selector' => static::append_to_selector( $selectors[ $name ]['elements'][ $element ], $pseudo_selector ), - ); - } - } - } - } - } + public function get_settings() { + if ( ! isset( $this->theme_json['settings'] ) ) { + return array(); + } else { + return $this->theme_json['settings']; } - - return $nodes; } /** * Returns the stylesheet that results of processing * the theme.json structure this object represents. * - * @param array $types Types of styles to load. Will load all by default. It accepts: - * 'variables': only the CSS Custom Properties for presets & custom ones. - * 'styles': only the styles section in theme.json. - * 'presets': only the classes for the presets. + * @since 5.8.0 + * @since 5.9.0 Removed the `$type` parameter`, added the `$types` and `$origins` parameters. + * + * @param array $types Types of styles to load. Will load all by default. It accepts: + * - `variables`: only the CSS Custom Properties for presets & custom ones. + * - `styles`: only the styles section in theme.json. + * - `presets`: only the classes for the presets. + * - `custom-css`: only the css from global styles.css. * @param array $origins A list of origins to include. By default it includes VALID_ORIGINS. * @param array $options An array of options for now used for internal purposes only (may change without notice). * The options currently supported are 'scope' that makes sure all style are scoped to a given selector, * and root_selector which overwrites and forces a given selector to be used on the root node. - * @return string Stylesheet. + * @return string The resulting stylesheet. */ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null, $options = array() ) { if ( null === $origins ) { @@ -693,7 +956,7 @@ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' if ( is_string( $types ) ) { // Dispatch error and map old arguments to new ones. - _deprecated_argument( __FUNCTION__, '5.9' ); + _deprecated_argument( __FUNCTION__, '5.9.0' ); if ( 'block_styles' === $types ) { $types = array( 'styles', 'presets' ); } elseif ( 'css_variables' === $types ) { @@ -772,601 +1035,2163 @@ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins ); } + // Load the custom CSS last so it has the highest specificity. + if ( in_array( 'custom-css', $types, true ) ) { + $stylesheet .= _wp_array_get( $this->theme_json, array( 'styles', 'css' ) ); + } + return $stylesheet; } - /** - * Returns a filtered declarations array if there is a separator block with only a background - * style defined in theme.json by adding a color attribute to reflect the changes in the front. + * Returns the page templates of the active theme. * - * @param array $declarations List of declarations. + * @since 5.9.0 * - * @return array $declarations List of declarations filtered. + * @return array */ - private static function update_separator_declarations( $declarations ) { - $background_matches = array_values( - array_filter( - $declarations, - function( $declaration ) { - return 'background-color' === $declaration['name']; - } - ) - ); - if ( ! empty( $background_matches && isset( $background_matches[0]['value'] ) ) ) { - $border_color_matches = array_values( - array_filter( - $declarations, - function( $declaration ) { - return 'border-color' === $declaration['name']; - } - ) - ); - $text_color_matches = array_values( - array_filter( - $declarations, - function( $declaration ) { - return 'color' === $declaration['name']; - } - ) - ); - if ( empty( $border_color_matches ) && empty( $text_color_matches ) ) { - $declarations[] = array( - 'name' => 'color', - 'value' => $background_matches[0]['value'], + public function get_custom_templates() { + $custom_templates = array(); + if ( ! isset( $this->theme_json['customTemplates'] ) || ! is_array( $this->theme_json['customTemplates'] ) ) { + return $custom_templates; + } + + foreach ( $this->theme_json['customTemplates'] as $item ) { + if ( isset( $item['name'] ) ) { + $custom_templates[ $item['name'] ] = array( + 'title' => isset( $item['title'] ) ? $item['title'] : '', + 'postTypes' => isset( $item['postTypes'] ) ? $item['postTypes'] : array( 'page' ), ); } } - - return $declarations; + return $custom_templates; } /** - * Gets the CSS rules for a particular block from theme.json. + * Returns the template part data of active theme. * - * @param array $block_metadata Metadata about the block to get styles for. + * @since 5.9.0 * - * @return string Styles for the block. + * @return array */ - public function get_styles_for_block( $block_metadata ) { - $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); - $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments']; - $selector = $block_metadata['selector']; - $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); - - // Process style declarations for block support features the current - // block contains selectors for. Values for a feature with a custom - // selector are filtered from the theme.json node before it is - // processed as normal. - $feature_declarations = array(); - - if ( ! empty( $block_metadata['features'] ) ) { - foreach ( $block_metadata['features'] as $feature_name => $feature_selector ) { - if ( ! empty( $node[ $feature_name ] ) ) { - // Create temporary node containing only the feature data - // to leverage existing `compute_style_properties` function. - $feature = array( $feature_name => $node[ $feature_name ] ); - // Generate the feature's declarations only. - $new_feature_declarations = static::compute_style_properties( $feature, $settings, null, $this->theme_json ); - - // Merge new declarations with any that already exist for - // the feature selector. This may occur when multiple block - // support features use the same custom selector. - if ( isset( $feature_declarations[ $feature_selector ] ) ) { - $feature_declarations[ $feature_selector ] = array_merge( $feature_declarations[ $feature_selector ], $new_feature_declarations ); - } else { - $feature_declarations[ $feature_selector ] = $new_feature_declarations; - } - - // Remove the feature from the block's node now the - // styles will be included under the feature level selector. - unset( $node[ $feature_name ] ); - } - } + public function get_template_parts() { + $template_parts = array(); + if ( ! isset( $this->theme_json['templateParts'] ) || ! is_array( $this->theme_json['templateParts'] ) ) { + return $template_parts; } - // Get a reference to element name from path. - // $block_metadata['path'] = array('styles','elements','link'); - // Make sure that $block_metadata['path'] describes an element node, like ['styles', 'element', 'link']. - // Skip non-element paths like just ['styles']. - $is_processing_element = in_array( 'elements', $block_metadata['path'], true ); - - $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null; - - $element_pseudo_allowed = array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ? static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] : array(); - - // Check for allowed pseudo classes (e.g. ":hover") from the $selector ("a:hover"). - // This also resets the array keys. - $pseudo_matches = array_values( - array_filter( - $element_pseudo_allowed, - function( $pseudo_selector ) use ( $selector ) { - return str_contains( $selector, $pseudo_selector ); - } - ) - ); - - $pseudo_selector = isset( $pseudo_matches[0] ) ? $pseudo_matches[0] : null; - - // If the current selector is a pseudo selector that's defined in the allow list for the current - // element then compute the style properties for it. - // Otherwise just compute the styles for the default selector as normal. - if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) && array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true ) ) { - $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json, $selector, $use_root_padding ); - } else { - $declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding ); + foreach ( $this->theme_json['templateParts'] as $item ) { + if ( isset( $item['name'] ) ) { + $template_parts[ $item['name'] ] = array( + 'title' => isset( $item['title'] ) ? $item['title'] : '', + 'area' => isset( $item['area'] ) ? $item['area'] : '', + ); + } } + return $template_parts; + } + /** + * Converts each style section into a list of rulesets + * containing the block styles to be appended to the stylesheet. + * + * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax + * + * For each section this creates a new ruleset such as: + * + * block-selector { + * style-property-one: value; + * } + * + * @since 5.8.0 As `get_block_styles()`. + * @since 5.9.0 Renamed from `get_block_styles()` to `get_block_classes()` + * and no longer returns preset classes. + * Removed the `$setting_nodes` parameter. + * @since 6.1.0 Moved most internal logic to `get_styles_for_block()`. + * + * @param array $style_nodes Nodes with styles. + * @return string The new stylesheet. + */ + protected function get_block_classes( $style_nodes ) { $block_rules = ''; - // 1. Separate the ones who use the general selector - // and the ones who use the duotone selector. - $declarations_duotone = array(); - foreach ( $declarations as $index => $declaration ) { - if ( 'filter' === $declaration['name'] ) { - unset( $declarations[ $index ] ); - $declarations_duotone[] = $declaration; + foreach ( $style_nodes as $metadata ) { + if ( null === $metadata['selector'] ) { + continue; } - } - - // Update declarations if there are separators with only background color defined. - if ( '.wp-block-separator' === $selector ) { - $declarations = static::update_separator_declarations( $declarations ); - } - - // 2. Generate and append the rules that use the general selector. - $block_rules .= static::to_ruleset( $selector, $declarations ); - - // 3. Generate and append the rules that use the duotone selector. - if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) { - $selector_duotone = static::scope_selector( $block_metadata['selector'], $block_metadata['duotone'] ); - $block_rules .= static::to_ruleset( $selector_duotone, $declarations_duotone ); - } - - // 4. Generate Layout block gap styles. - if ( - static::ROOT_BLOCK_SELECTOR !== $selector && - ! empty( $block_metadata['name'] ) - ) { - $block_rules .= $this->get_layout_styles( $block_metadata ); - } - - // 5. Generate and append the feature level rulesets. - foreach ( $feature_declarations as $feature_selector => $individual_feature_declarations ) { - $block_rules .= static::to_ruleset( $feature_selector, $individual_feature_declarations ); + $block_rules .= static::get_styles_for_block( $metadata ); } return $block_rules; } /** - * Outputs the CSS for layout rules on the root. + * Gets the CSS layout rules for a particular block from theme.json layout definitions. * - * @param string $selector The root node selector. - * @param array $block_metadata The metadata for the root block. - * @return string The additional root rules CSS. + * @since 6.1.0 + * + * @param array $block_metadata Metadata about the block to get styles for. + * @return string Layout styles for the block. */ - public function get_root_layout_rules( $selector, $block_metadata ) { - $css = ''; - $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); - $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments']; - - /* - * Reset default browser margin on the root body element. - * This is set on the root selector **before** generating the ruleset - * from the `theme.json`. This is to ensure that if the `theme.json` declares - * `margin` in its `spacing` declaration for the `body` element then these - * user-generated values take precedence in the CSS cascade. - * @link https://github.com/WordPress/gutenberg/issues/36147. - */ - $css .= 'body { margin: 0;'; + protected function get_layout_styles( $block_metadata ) { + $block_rules = ''; + $block_type = null; - /* - * If there are content and wide widths in theme.json, output them - * as custom properties on the body element so all blocks can use them. - */ - if ( isset( $settings['layout']['contentSize'] ) || isset( $settings['layout']['wideSize'] ) ) { - $content_size = isset( $settings['layout']['contentSize'] ) ? $settings['layout']['contentSize'] : $settings['layout']['wideSize']; - $content_size = static::is_safe_css_declaration( 'max-width', $content_size ) ? $content_size : 'initial'; - $wide_size = isset( $settings['layout']['wideSize'] ) ? $settings['layout']['wideSize'] : $settings['layout']['contentSize']; - $wide_size = static::is_safe_css_declaration( 'max-width', $wide_size ) ? $wide_size : 'initial'; - $css .= '--wp--style--global--content-size: ' . $content_size . ';'; - $css .= '--wp--style--global--wide-size: ' . $wide_size . ';'; + // Skip outputting layout styles if explicitly disabled. + if ( current_theme_supports( 'disable-layout-styles' ) ) { + return $block_rules; } - $css .= '}'; + if ( isset( $block_metadata['name'] ) ) { + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_metadata['name'] ); + if ( ! block_has_support( $block_type, array( '__experimentalLayout' ), false ) ) { + return $block_rules; + } + } + + $selector = isset( $block_metadata['selector'] ) ? $block_metadata['selector'] : ''; + $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null; + $has_fallback_gap_support = ! $has_block_gap_support; // This setting isn't useful yet: it exists as a placeholder for a future explicit fallback gap styles support. + $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); + $layout_definitions = _wp_array_get( $this->theme_json, array( 'settings', 'layout', 'definitions' ), array() ); + $layout_selector_pattern = '/^[a-zA-Z0-9\-\.\ *+>:\(\)]*$/'; // Allow alphanumeric classnames, spaces, wildcard, sibling, child combinator and pseudo class selectors. + + // Gap styles will only be output if the theme has block gap support, or supports a fallback gap. + // Default layout gap styles will be skipped for themes that do not explicitly opt-in to blockGap with a `true` or `false` value. + if ( $has_block_gap_support || $has_fallback_gap_support ) { + $block_gap_value = null; + // Use a fallback gap value if block gap support is not available. + if ( ! $has_block_gap_support ) { + $block_gap_value = static::ROOT_BLOCK_SELECTOR === $selector ? '0.5em' : null; + if ( ! empty( $block_type ) ) { + $block_gap_value = _wp_array_get( $block_type->supports, array( 'spacing', 'blockGap', '__experimentalDefault' ), null ); + } + } else { + $block_gap_value = static::get_property_value( $node, array( 'spacing', 'blockGap' ) ); + } + + // Support split row / column values and concatenate to a shorthand value. + if ( is_array( $block_gap_value ) ) { + if ( isset( $block_gap_value['top'] ) && isset( $block_gap_value['left'] ) ) { + $gap_row = static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) ); + $gap_column = static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) ); + $block_gap_value = $gap_row === $gap_column ? $gap_row : $gap_row . ' ' . $gap_column; + } else { + // Skip outputting gap value if not all sides are provided. + $block_gap_value = null; + } + } + + // If the block should have custom gap, add the gap styles. + if ( null !== $block_gap_value && false !== $block_gap_value && '' !== $block_gap_value ) { + foreach ( $layout_definitions as $layout_definition_key => $layout_definition ) { + // Allow outputting fallback gap styles for flex layout type when block gap support isn't available. + if ( ! $has_block_gap_support && 'flex' !== $layout_definition_key ) { + continue; + } + + $class_name = sanitize_title( _wp_array_get( $layout_definition, array( 'className' ), false ) ); + $spacing_rules = _wp_array_get( $layout_definition, array( 'spacingStyles' ), array() ); + + if ( + ! empty( $class_name ) && + ! empty( $spacing_rules ) + ) { + foreach ( $spacing_rules as $spacing_rule ) { + $declarations = array(); + if ( + isset( $spacing_rule['selector'] ) && + preg_match( $layout_selector_pattern, $spacing_rule['selector'] ) && + ! empty( $spacing_rule['rules'] ) + ) { + // Iterate over each of the styling rules and substitute non-string values such as `null` with the real `blockGap` value. + foreach ( $spacing_rule['rules'] as $css_property => $css_value ) { + $current_css_value = is_string( $css_value ) ? $css_value : $block_gap_value; + if ( static::is_safe_css_declaration( $css_property, $current_css_value ) ) { + $declarations[] = array( + 'name' => $css_property, + 'value' => $current_css_value, + ); + } + } + + if ( ! $has_block_gap_support ) { + // For fallback gap styles, use lower specificity, to ensure styles do not unintentionally override theme styles. + $format = static::ROOT_BLOCK_SELECTOR === $selector ? ':where(.%2$s%3$s)' : ':where(%1$s.%2$s%3$s)'; + $layout_selector = sprintf( + $format, + $selector, + $class_name, + $spacing_rule['selector'] + ); + } else { + $format = static::ROOT_BLOCK_SELECTOR === $selector ? '%s .%s%s' : '%s.%s%s'; + $layout_selector = sprintf( + $format, + $selector, + $class_name, + $spacing_rule['selector'] + ); + } + $block_rules .= static::to_ruleset( $layout_selector, $declarations ); + } + } + } + } + } + } + + // Output base styles. + if ( + static::ROOT_BLOCK_SELECTOR === $selector + ) { + $valid_display_modes = array( 'block', 'flex', 'grid' ); + foreach ( $layout_definitions as $layout_definition ) { + $class_name = sanitize_title( _wp_array_get( $layout_definition, array( 'className' ), false ) ); + $base_style_rules = _wp_array_get( $layout_definition, array( 'baseStyles' ), array() ); + + if ( + ! empty( $class_name ) && + ! empty( $base_style_rules ) + ) { + // Output display mode. This requires special handling as `display` is not exposed in `safe_style_css_filter`. + if ( + ! empty( $layout_definition['displayMode'] ) && + is_string( $layout_definition['displayMode'] ) && + in_array( $layout_definition['displayMode'], $valid_display_modes, true ) + ) { + $layout_selector = sprintf( + '%s .%s', + $selector, + $class_name + ); + $block_rules .= static::to_ruleset( + $layout_selector, + array( + array( + 'name' => 'display', + 'value' => $layout_definition['displayMode'], + ), + ) + ); + } + + foreach ( $base_style_rules as $base_style_rule ) { + $declarations = array(); + + if ( + isset( $base_style_rule['selector'] ) && + preg_match( $layout_selector_pattern, $base_style_rule['selector'] ) && + ! empty( $base_style_rule['rules'] ) + ) { + foreach ( $base_style_rule['rules'] as $css_property => $css_value ) { + if ( static::is_safe_css_declaration( $css_property, $css_value ) ) { + $declarations[] = array( + 'name' => $css_property, + 'value' => $css_value, + ); + } + } + + $layout_selector = sprintf( + '%s .%s%s', + $selector, + $class_name, + $base_style_rule['selector'] + ); + $block_rules .= static::to_ruleset( $layout_selector, $declarations ); + } + } + } + } + } + return $block_rules; + } + + /** + * Creates new rulesets as classes for each preset value such as: + * + * .has-value-color { + * color: value; + * } + * + * .has-value-background-color { + * background-color: value; + * } + * + * .has-value-font-size { + * font-size: value; + * } + * + * .has-value-gradient-background { + * background: value; + * } + * + * p.has-value-gradient-background { + * background: value; + * } + * + * @since 5.9.0 + * + * @param array $setting_nodes Nodes with settings. + * @param array $origins List of origins to process presets from. + * @return string The new stylesheet. + */ + protected function get_preset_classes( $setting_nodes, $origins ) { + $preset_rules = ''; + + foreach ( $setting_nodes as $metadata ) { + if ( null === $metadata['selector'] ) { + continue; + } + + $selector = $metadata['selector']; + $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); + $preset_rules .= static::compute_preset_classes( $node, $selector, $origins ); + } + + return $preset_rules; + } + + /** + * Converts each styles section into a list of rulesets + * to be appended to the stylesheet. + * These rulesets contain all the css variables (custom variables and preset variables). + * + * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax + * + * For each section this creates a new ruleset such as: + * + * block-selector { + * --wp--preset--category--slug: value; + * --wp--custom--variable: value; + * } + * + * @since 5.8.0 + * @since 5.9.0 Added the `$origins` parameter. + * + * @param array $nodes Nodes with settings. + * @param array $origins List of origins to process. + * @return string The new stylesheet. + */ + protected function get_css_variables( $nodes, $origins ) { + $stylesheet = ''; + foreach ( $nodes as $metadata ) { + if ( null === $metadata['selector'] ) { + continue; + } + + $selector = $metadata['selector']; + + $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); + $declarations = static::compute_preset_vars( $node, $origins ); + $theme_vars_declarations = static::compute_theme_vars( $node ); + foreach ( $theme_vars_declarations as $theme_vars_declaration ) { + $declarations[] = $theme_vars_declaration; + } + + $stylesheet .= static::to_ruleset( $selector, $declarations ); + } + + return $stylesheet; + } + + /** + * Given a selector and a declaration list, + * creates the corresponding ruleset. + * + * @since 5.8.0 + * + * @param string $selector CSS selector. + * @param array $declarations List of declarations. + * @return string The resulting CSS ruleset. + */ + protected static function to_ruleset( $selector, $declarations ) { + if ( empty( $declarations ) ) { + return ''; + } + + $declaration_block = array_reduce( + $declarations, + static function ( $carry, $element ) { + return $carry .= $element['name'] . ': ' . $element['value'] . ';'; }, + '' + ); + + return $selector . '{' . $declaration_block . '}'; + } + + /** + * Given a settings array, returns the generated rulesets + * for the preset classes. + * + * @since 5.8.0 + * @since 5.9.0 Added the `$origins` parameter. + * + * @param array $settings Settings to process. + * @param string $selector Selector wrapping the classes. + * @param array $origins List of origins to process. + * @return string The result of processing the presets. + */ + protected static function compute_preset_classes( $settings, $selector, $origins ) { + if ( static::ROOT_BLOCK_SELECTOR === $selector ) { + // Classes at the global level do not need any CSS prefixed, + // and we don't want to increase its specificity. + $selector = ''; + } + + $stylesheet = ''; + foreach ( static::PRESETS_METADATA as $preset_metadata ) { + $slugs = static::get_settings_slugs( $settings, $preset_metadata, $origins ); + foreach ( $preset_metadata['classes'] as $class => $property ) { + foreach ( $slugs as $slug ) { + $css_var = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ); + $class_name = static::replace_slug_in_string( $class, $slug ); + $stylesheet .= static::to_ruleset( + static::append_to_selector( $selector, $class_name ), + array( + array( + 'name' => $property, + 'value' => 'var(' . $css_var . ') !important', + ), + ) + ); + } + } + } + + return $stylesheet; + } + + /** + * Function that scopes a selector with another one. This works a bit like + * SCSS nesting except the `&` operator isn't supported. + * + * + * $scope = '.a, .b .c'; + * $selector = '> .x, .y'; + * $merged = scope_selector( $scope, $selector ); + * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y' + * + * + * @since 5.9.0 + * + * @param string $scope Selector to scope to. + * @param string $selector Original selector. + * @return string Scoped selector. + */ + protected static function scope_selector( $scope, $selector ) { + $scopes = explode( ',', $scope ); + $selectors = explode( ',', $selector ); + + $selectors_scoped = array(); + foreach ( $scopes as $outer ) { + foreach ( $selectors as $inner ) { + $outer = trim( $outer ); + $inner = trim( $inner ); + if ( ! empty( $outer ) && ! empty( $inner ) ) { + $selectors_scoped[] = $outer . ' ' . $inner; + } elseif ( empty( $outer ) ) { + $selectors_scoped[] = $inner; + } elseif ( empty( $inner ) ) { + $selectors_scoped[] = $outer; + } + } + } + + $result = implode( ', ', $selectors_scoped ); + return $result; + } + + /** + * Gets preset values keyed by slugs based on settings and metadata. + * + * + * $settings = array( + * 'typography' => array( + * 'fontFamilies' => array( + * array( + * 'slug' => 'sansSerif', + * 'fontFamily' => '"Helvetica Neue", sans-serif', + * ), + * array( + * 'slug' => 'serif', + * 'colors' => 'Georgia, serif', + * ) + * ), + * ), + * ); + * $meta = array( + * 'path' => array( 'typography', 'fontFamilies' ), + * 'value_key' => 'fontFamily', + * ); + * $values_by_slug = get_settings_values_by_slug(); + * // $values_by_slug === array( + * // 'sans-serif' => '"Helvetica Neue", sans-serif', + * // 'serif' => 'Georgia, serif', + * // ); + * + * + * @since 5.9.0 + * + * @param array $settings Settings to process. + * @param array $preset_metadata One of the PRESETS_METADATA values. + * @param array $origins List of origins to process. + * @return array Array of presets where each key is a slug and each value is the preset value. + */ + protected static function get_settings_values_by_slug( $settings, $preset_metadata, $origins ) { + $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() ); + + $result = array(); + foreach ( $origins as $origin ) { + if ( ! isset( $preset_per_origin[ $origin ] ) ) { + continue; + } + foreach ( $preset_per_origin[ $origin ] as $preset ) { + $slug = _wp_to_kebab_case( $preset['slug'] ); + + $value = ''; + if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) { + $value_key = $preset_metadata['value_key']; + $value = $preset[ $value_key ]; + } elseif ( + isset( $preset_metadata['value_func'] ) && + is_callable( $preset_metadata['value_func'] ) + ) { + $value_func = $preset_metadata['value_func']; + $value = call_user_func( $value_func, $preset ); + } else { + // If we don't have a value, then don't add it to the result. + continue; + } + + $result[ $slug ] = $value; + } + } + return $result; + } + + /** + * Similar to get_settings_values_by_slug, but doesn't compute the value. + * + * @since 5.9.0 + * + * @param array $settings Settings to process. + * @param array $preset_metadata One of the PRESETS_METADATA values. + * @param array $origins List of origins to process. + * @return array Array of presets where the key and value are both the slug. + */ + protected static function get_settings_slugs( $settings, $preset_metadata, $origins = null ) { + if ( null === $origins ) { + $origins = static::VALID_ORIGINS; + } + + $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() ); + + $result = array(); + foreach ( $origins as $origin ) { + if ( ! isset( $preset_per_origin[ $origin ] ) ) { + continue; + } + foreach ( $preset_per_origin[ $origin ] as $preset ) { + $slug = _wp_to_kebab_case( $preset['slug'] ); + + // Use the array as a set so we don't get duplicates. + $result[ $slug ] = $slug; + } + } + return $result; + } + + /** + * Transforms a slug into a CSS Custom Property. + * + * @since 5.9.0 + * + * @param string $input String to replace. + * @param string $slug The slug value to use to generate the custom property. + * @return string The CSS Custom Property. Something along the lines of `--wp--preset--color--black`. + */ + protected static function replace_slug_in_string( $input, $slug ) { + return strtr( $input, array( '$slug' => $slug ) ); + } + + /** + * Given the block settings, extracts the CSS Custom Properties + * for the presets and adds them to the $declarations array + * following the format: + * + * ```php + * array( + * 'name' => 'property_name', + * 'value' => 'property_value, + * ) + * ``` + * + * @since 5.8.0 + * @since 5.9.0 Added the `$origins` parameter. + * + * @param array $settings Settings to process. + * @param array $origins List of origins to process. + * @return array The modified $declarations. + */ + protected static function compute_preset_vars( $settings, $origins ) { + $declarations = array(); + foreach ( static::PRESETS_METADATA as $preset_metadata ) { + $values_by_slug = static::get_settings_values_by_slug( $settings, $preset_metadata, $origins ); + foreach ( $values_by_slug as $slug => $value ) { + $declarations[] = array( + 'name' => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ), + 'value' => $value, + ); + } + } + + return $declarations; + } + + /** + * Given an array of settings, extracts the CSS Custom Properties + * for the custom values and adds them to the $declarations + * array following the format: + * + * ```php + * array( + * 'name' => 'property_name', + * 'value' => 'property_value, + * ) + * ``` + * + * @since 5.8.0 + * + * @param array $settings Settings to process. + * @return array The modified $declarations. + */ + protected static function compute_theme_vars( $settings ) { + $declarations = array(); + $custom_values = _wp_array_get( $settings, array( 'custom' ), array() ); + $css_vars = static::flatten_tree( $custom_values ); + foreach ( $css_vars as $key => $value ) { + $declarations[] = array( + 'name' => '--wp--custom--' . $key, + 'value' => $value, + ); + } + + return $declarations; + } + + /** + * Given a tree, it creates a flattened one + * by merging the keys and binding the leaf values + * to the new keys. + * + * It also transforms camelCase names into kebab-case + * and substitutes '/' by '-'. + * + * This is thought to be useful to generate + * CSS Custom Properties from a tree, + * although there's nothing in the implementation + * of this function that requires that format. + * + * For example, assuming the given prefix is '--wp' + * and the token is '--', for this input tree: + * + * { + * 'some/property': 'value', + * 'nestedProperty': { + * 'sub-property': 'value' + * } + * } + * + * it'll return this output: + * + * { + * '--wp--some-property': 'value', + * '--wp--nested-property--sub-property': 'value' + * } + * + * @since 5.8.0 + * + * @param array $tree Input tree to process. + * @param string $prefix Optional. Prefix to prepend to each variable. Default empty string. + * @param string $token Optional. Token to use between levels. Default '--'. + * @return array The flattened tree. + */ + protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) { + $result = array(); + foreach ( $tree as $property => $value ) { + $new_key = $prefix . str_replace( + '/', + '-', + strtolower( _wp_to_kebab_case( $property ) ) + ); + + if ( is_array( $value ) ) { + $new_prefix = $new_key . $token; + $flattened_subtree = static::flatten_tree( $value, $new_prefix, $token ); + foreach ( $flattened_subtree as $subtree_key => $subtree_value ) { + $result[ $subtree_key ] = $subtree_value; + } + } else { + $result[ $new_key ] = $value; + } + } + return $result; + } + + /** + * Given a styles array, it extracts the style properties + * and adds them to the $declarations array following the format: + * + * ```php + * array( + * 'name' => 'property_name', + * 'value' => 'property_value, + * ) + * ``` + * + * @since 5.8.0 + * @since 5.9.0 Added the `$settings` and `$properties` parameters. + * @since 6.1.0 Added `$theme_json`, `$selector`, and `$use_root_padding` parameters. + * + * @param array $styles Styles to process. + * @param array $settings Theme settings. + * @param array $properties Properties metadata. + * @param array $theme_json Theme JSON array. + * @param string $selector The style block selector. + * @param boolean $use_root_padding Whether to add custom properties at root level. + * @return array Returns the modified $declarations. + */ + protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) { + if ( null === $properties ) { + $properties = static::PROPERTIES_METADATA; + } + + $declarations = array(); + if ( empty( $styles ) ) { + return $declarations; + } + + $root_variable_duplicates = array(); + + foreach ( $properties as $css_property => $value_path ) { + $value = static::get_property_value( $styles, $value_path, $theme_json ); + + if ( str_starts_with( $css_property, '--wp--style--root--' ) && ( static::ROOT_BLOCK_SELECTOR !== $selector || ! $use_root_padding ) ) { + continue; + } + // Root-level padding styles don't currently support strings with CSS shorthand values. + // This may change: https://github.com/WordPress/gutenberg/issues/40132. + if ( '--wp--style--root--padding' === $css_property && is_string( $value ) ) { + continue; + } + + if ( str_starts_with( $css_property, '--wp--style--root--' ) && $use_root_padding ) { + $root_variable_duplicates[] = substr( $css_property, strlen( '--wp--style--root--' ) ); + } + + // Look up protected properties, keyed by value path. + // Skip protected properties that are explicitly set to `null`. + if ( is_array( $value_path ) ) { + $path_string = implode( '.', $value_path ); + if ( + // TODO: Replace array_key_exists() with isset() check once WordPress drops + // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. + array_key_exists( $path_string, static::PROTECTED_PROPERTIES ) && + _wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null + ) { + continue; + } + } + + // Skip if empty and not "0" or value represents array of longhand values. + $has_missing_value = empty( $value ) && ! is_numeric( $value ); + if ( $has_missing_value || is_array( $value ) ) { + continue; + } + + // Calculates fluid typography rules where available. + if ( 'font-size' === $css_property ) { + /* + * wp_get_typography_font_size_value() will check + * if fluid typography has been activated and also + * whether the incoming value can be converted to a fluid value. + * Values that already have a clamp() function will not pass the test, + * and therefore the original $value will be returned. + */ + $value = gutenberg_get_typography_font_size_value( array( 'size' => $value ) ); + } + + $declarations[] = array( + 'name' => $css_property, + 'value' => $value, + ); + } + + // If a variable value is added to the root, the corresponding property should be removed. + foreach ( $root_variable_duplicates as $duplicate ) { + $discard = array_search( $duplicate, array_column( $declarations, 'name' ), true ); + if ( is_numeric( $discard ) ) { + array_splice( $declarations, $discard, 1 ); + } + } + + return $declarations; + } + + /** + * Returns the style property for the given path. + * + * It also converts CSS Custom Property stored as + * "var:preset|color|secondary" to the form + * "--wp--preset--color--secondary". + * + * It also converts references to a path to the value + * stored at that location, e.g. + * { "ref": "style.color.background" } => "#fff". + * + * @since 5.8.0 + * @since 5.9.0 Added support for values of array type, which are returned as is. + * @since 6.1.0 Added the `$theme_json` parameter. + * + * @param array $styles Styles subtree. + * @param array $path Which property to process. + * @param array $theme_json Theme JSON array. + * @return string|array Style property value. + */ + protected static function get_property_value( $styles, $path, $theme_json = null ) { + $value = _wp_array_get( $styles, $path, '' ); + + // Gutenberg didn't have this check. + if ( '' === $value || null === $value ) { + // No need to process the value further. + return ''; + } + + /* + * This converts references to a path to the value at that path + * where the values is an array with a "ref" key, pointing to a path. + * For example: { "ref": "style.color.background" } => "#fff". + */ + if ( is_array( $value ) && isset( $value['ref'] ) ) { + $value_path = explode( '.', $value['ref'] ); + $ref_value = _wp_array_get( $theme_json, $value_path ); + // Only use the ref value if we find anything. + if ( ! empty( $ref_value ) && is_string( $ref_value ) ) { + $value = $ref_value; + } + + if ( is_array( $ref_value ) && isset( $ref_value['ref'] ) ) { + $path_string = json_encode( $path ); + $ref_value_string = json_encode( $ref_value ); + _doing_it_wrong( + 'get_property_value', + sprintf( + /* translators: 1: theme.json, 2: Value name, 3: Value path, 4: Another value name. */ + __( 'Your %1$s file uses a dynamic value (%2$s) for the path at %3$s. However, the value at %3$s is also a dynamic value (pointing to %4$s) and pointing to another dynamic value is not supported. Please update %3$s to point directly to %4$s.', 'gutenberg' ), + 'theme.json', + $ref_value_string, + $path_string, + $ref_value['ref'] + ), + '6.1.0' + ); + } + } + + if ( is_array( $value ) ) { + return $value; + } + + // Convert custom CSS properties. + $prefix = 'var:'; + $prefix_len = strlen( $prefix ); + $token_in = '|'; + $token_out = '--'; + if ( 0 === strncmp( $value, $prefix, $prefix_len ) ) { + $unwrapped_name = str_replace( + $token_in, + $token_out, + substr( $value, $prefix_len ) + ); + $value = "var(--wp--$unwrapped_name)"; + } + + return $value; + } + + /** + * Builds metadata for the setting nodes, which returns in the form of: + * + * [ + * [ + * 'path' => ['path', 'to', 'some', 'node' ], + * 'selector' => 'CSS selector for some node' + * ], + * [ + * 'path' => [ 'path', 'to', 'other', 'node' ], + * 'selector' => 'CSS selector for other node' + * ], + * ] + * + * @since 5.8.0 + * + * @param array $theme_json The tree to extract setting nodes from. + * @param array $selectors List of selectors per block. + * @return array An array of setting nodes metadata. + */ + protected static function get_setting_nodes( $theme_json, $selectors = array() ) { + $nodes = array(); + if ( ! isset( $theme_json['settings'] ) ) { + return $nodes; + } + + // Top-level. + $nodes[] = array( + 'path' => array( 'settings' ), + 'selector' => static::ROOT_BLOCK_SELECTOR, + ); + + // Calculate paths for blocks. + if ( ! isset( $theme_json['settings']['blocks'] ) ) { + return $nodes; + } + + foreach ( $theme_json['settings']['blocks'] as $name => $node ) { + $selector = null; + if ( isset( $selectors[ $name ]['selector'] ) ) { + $selector = $selectors[ $name ]['selector']; + } + + $nodes[] = array( + 'path' => array( 'settings', 'blocks', $name ), + 'selector' => $selector, + ); + } + + return $nodes; + } + + /** + * Builds metadata for the style nodes, which returns in the form of: + * + * [ + * [ + * 'path' => [ 'path', 'to', 'some', 'node' ], + * 'selector' => 'CSS selector for some node', + * 'duotone' => 'CSS selector for duotone for some node' + * ], + * [ + * 'path' => ['path', 'to', 'other', 'node' ], + * 'selector' => 'CSS selector for other node', + * 'duotone' => null + * ], + * ] + * + * @since 5.8.0 + * + * @param array $theme_json The tree to extract style nodes from. + * @param array $selectors List of selectors per block. + * @return array An array of style nodes metadata. + */ + protected static function get_style_nodes( $theme_json, $selectors = array() ) { + $nodes = array(); + if ( ! isset( $theme_json['styles'] ) ) { + return $nodes; + } + + // Top-level. + $nodes[] = array( + 'path' => array( 'styles' ), + 'selector' => static::ROOT_BLOCK_SELECTOR, + ); + + if ( isset( $theme_json['styles']['elements'] ) ) { + foreach ( self::ELEMENTS as $element => $selector ) { + if ( ! isset( $theme_json['styles']['elements'][ $element ] ) || ! array_key_exists( $element, static::ELEMENTS ) ) { + continue; + } + + // Handle element defaults. + $nodes[] = array( + 'path' => array( 'styles', 'elements', $element ), + 'selector' => static::ELEMENTS[ $element ], + ); + + // Handle any pseudo selectors for the element. + // TODO: Replace array_key_exists() with isset() check once WordPress drops + // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. + if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { + + if ( isset( $theme_json['styles']['elements'][ $element ][ $pseudo_selector ] ) ) { + $nodes[] = array( + 'path' => array( 'styles', 'elements', $element ), + 'selector' => static::append_to_selector( static::ELEMENTS[ $element ], $pseudo_selector ), + ); + } + } + } + } + } + + // Blocks. + if ( ! isset( $theme_json['styles']['blocks'] ) ) { + return $nodes; + } + + $block_nodes = static::get_block_nodes( $theme_json, $selectors ); + foreach ( $block_nodes as $block_node ) { + $nodes[] = $block_node; + } + + /** + * Filters the list of style nodes with metadata. + * + * This allows for things like loading block CSS independently. + * + * @since 6.1.0 + * + * @param array $nodes Style nodes with metadata. + */ + return apply_filters( 'wp_theme_json_get_style_nodes', $nodes ); + } + + /** + * A public helper to get the block nodes from a theme.json file. + * + * @since 6.1.0 + * + * @return array The block nodes in theme.json. + */ + public function get_styles_block_nodes() { + return static::get_block_nodes( $this->theme_json ); + } + + /** + * Returns a filtered declarations array if there is a separator block with only a background + * style defined in theme.json by adding a color attribute to reflect the changes in the front. + * + * @since 6.1.1 + * + * @param array $declarations List of declarations. + * @return array $declarations List of declarations filtered. + */ + private static function update_separator_declarations( $declarations ) { + // Gutenberg and core implementation differed. + // https://github.com/WordPress/gutenberg/pull/44943. + $background_color = ''; + $border_color_matches = false; + $text_color_matches = false; + + foreach ( $declarations as $declaration ) { + if ( 'background-color' === $declaration['name'] && ! $background_color && isset( $declaration['value'] ) ) { + $background_color = $declaration['value']; + } elseif ( 'border-color' === $declaration['name'] ) { + $border_color_matches = true; + } elseif ( 'color' === $declaration['name'] ) { + $text_color_matches = true; + } + + if ( $background_color && $border_color_matches && $text_color_matches ) { + break; + } + } + + if ( $background_color && ! $border_color_matches && ! $text_color_matches ) { + $declarations[] = array( + 'name' => 'color', + 'value' => $background_color, + ); + } + + return $declarations; + } + + /** + * An internal method to get the block nodes from a theme.json file. + * + * @since 6.1.0 + * + * @param array $theme_json The theme.json converted to an array. + * @param array $selectors Optional list of selectors per block. + * @return array The block nodes in theme.json. + */ + private static function get_block_nodes( $theme_json, $selectors = array() ) { + $selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors; + $nodes = array(); + if ( ! isset( $theme_json['styles'] ) ) { + return $nodes; + } + + // Blocks. + if ( ! isset( $theme_json['styles']['blocks'] ) ) { + return $nodes; + } + + foreach ( $theme_json['styles']['blocks'] as $name => $node ) { + $selector = null; + if ( isset( $selectors[ $name ]['selector'] ) ) { + $selector = $selectors[ $name ]['selector']; + } + + $duotone_selector = null; + if ( isset( $selectors[ $name ]['duotone'] ) ) { + $duotone_selector = $selectors[ $name ]['duotone']; + } + + $feature_selectors = null; + if ( isset( $selectors[ $name ]['features'] ) ) { + $feature_selectors = $selectors[ $name ]['features']; + } + + $nodes[] = array( + 'name' => $name, + 'path' => array( 'styles', 'blocks', $name ), + 'selector' => $selector, + 'duotone' => $duotone_selector, + 'features' => $feature_selectors, + ); + + if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) { + foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) { + $nodes[] = array( + 'path' => array( 'styles', 'blocks', $name, 'elements', $element ), + 'selector' => $selectors[ $name ]['elements'][ $element ], + ); + + // Handle any pseudo selectors for the element. + // TODO: Replace array_key_exists() with isset() check once WordPress drops + // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. + if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { + if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) { + $nodes[] = array( + 'path' => array( 'styles', 'blocks', $name, 'elements', $element ), + 'selector' => static::append_to_selector( $selectors[ $name ]['elements'][ $element ], $pseudo_selector ), + ); + } + } + } + } + } + } + + return $nodes; + } + + /** + * Gets the CSS rules for a particular block from theme.json. + * + * @since 6.1.0 + * + * @param array $block_metadata Metadata about the block to get styles for. + * + * @return string Styles for the block. + */ + public function get_styles_for_block( $block_metadata ) { + $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); + $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments']; + $selector = $block_metadata['selector']; + $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); + + /* + * Process style declarations for block support features the current + * block contains selectors for. Values for a feature with a custom + * selector are filtered from the theme.json node before it is + * processed as normal. + */ + $feature_declarations = array(); + + if ( ! empty( $block_metadata['features'] ) ) { + foreach ( $block_metadata['features'] as $feature_name => $feature_selector ) { + if ( ! empty( $node[ $feature_name ] ) ) { + // Create temporary node containing only the feature data + // to leverage existing `compute_style_properties` function. + $feature = array( $feature_name => $node[ $feature_name ] ); + // Generate the feature's declarations only. + $new_feature_declarations = static::compute_style_properties( $feature, $settings, null, $this->theme_json ); + + // Merge new declarations with any that already exist for + // the feature selector. This may occur when multiple block + // support features use the same custom selector. + if ( isset( $feature_declarations[ $feature_selector ] ) ) { + foreach ( $new_feature_declarations as $new_feature_declaration ) { + $feature_declarations[ $feature_selector ][] = $new_feature_declaration; + } + } else { + $feature_declarations[ $feature_selector ] = $new_feature_declarations; + } + + // Remove the feature from the block's node now the + // styles will be included under the feature level selector. + unset( $node[ $feature_name ] ); + } + } + } + + /* + * Get a reference to element name from path. + * $block_metadata['path'] = array( 'styles','elements','link' ); + * Make sure that $block_metadata['path'] describes an element node, like [ 'styles', 'element', 'link' ]. + * Skip non-element paths like just ['styles']. + */ + $is_processing_element = in_array( 'elements', $block_metadata['path'], true ); + + $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null; + + $element_pseudo_allowed = array(); + + // TODO: Replace array_key_exists() with isset() check once WordPress drops + // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. + if ( array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { + $element_pseudo_allowed = static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ]; + } + + /* + * Check for allowed pseudo classes (e.g. ":hover") from the $selector ("a:hover"). + * This also resets the array keys. + */ + $pseudo_matches = array_values( + array_filter( + $element_pseudo_allowed, + function( $pseudo_selector ) use ( $selector ) { + return str_contains( $selector, $pseudo_selector ); + } + ) + ); + + $pseudo_selector = isset( $pseudo_matches[0] ) ? $pseudo_matches[0] : null; + + /* + * If the current selector is a pseudo selector that's defined in the allow list for the current + * element then compute the style properties for it. + * Otherwise just compute the styles for the default selector as normal. + */ + if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) && + // TODO: Replace array_key_exists() with isset() check once WordPress drops + // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. + array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) + && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true ) + ) { + $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json, $selector, $use_root_padding ); + } else { + $declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding ); + } + + $block_rules = ''; + + /* + * 1. Separate the declarations that use the general selector + * from the ones using the duotone selector. + */ + $declarations_duotone = array(); + foreach ( $declarations as $index => $declaration ) { + if ( 'filter' === $declaration['name'] ) { + unset( $declarations[ $index ] ); + $declarations_duotone[] = $declaration; + } + } + + // Update declarations if there are separators with only background color defined. + if ( '.wp-block-separator' === $selector ) { + $declarations = static::update_separator_declarations( $declarations ); + } + + // 2. Generate and append the rules that use the general selector. + $block_rules .= static::to_ruleset( $selector, $declarations ); + + // 3. Generate and append the rules that use the duotone selector. + if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) { + $selector_duotone = static::scope_selector( $block_metadata['selector'], $block_metadata['duotone'] ); + $block_rules .= static::to_ruleset( $selector_duotone, $declarations_duotone ); + } + + // 4. Generate Layout block gap styles. + if ( + static::ROOT_BLOCK_SELECTOR !== $selector && + ! empty( $block_metadata['name'] ) + ) { + $block_rules .= $this->get_layout_styles( $block_metadata ); + } + + // 5. Generate and append the feature level rulesets. + foreach ( $feature_declarations as $feature_selector => $individual_feature_declarations ) { + $block_rules .= static::to_ruleset( $feature_selector, $individual_feature_declarations ); + } + + return $block_rules; + } + + /** + * Outputs the CSS for layout rules on the root. + * + * @since 6.1.0 + * + * @param string $selector The root node selector. + * @param array $block_metadata The metadata for the root block. + * @return string The additional root rules CSS. + */ + public function get_root_layout_rules( $selector, $block_metadata ) { + $css = ''; + $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); + $use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments']; + + /* + * Reset default browser margin on the root body element. + * This is set on the root selector **before** generating the ruleset + * from the `theme.json`. This is to ensure that if the `theme.json` declares + * `margin` in its `spacing` declaration for the `body` element then these + * user-generated values take precedence in the CSS cascade. + * @link https://github.com/WordPress/gutenberg/issues/36147. + */ + $css .= 'body { margin: 0;'; + + /* + * If there are content and wide widths in theme.json, output them + * as custom properties on the body element so all blocks can use them. + */ + if ( isset( $settings['layout']['contentSize'] ) || isset( $settings['layout']['wideSize'] ) ) { + $content_size = isset( $settings['layout']['contentSize'] ) ? $settings['layout']['contentSize'] : $settings['layout']['wideSize']; + $content_size = static::is_safe_css_declaration( 'max-width', $content_size ) ? $content_size : 'initial'; + $wide_size = isset( $settings['layout']['wideSize'] ) ? $settings['layout']['wideSize'] : $settings['layout']['contentSize']; + $wide_size = static::is_safe_css_declaration( 'max-width', $wide_size ) ? $wide_size : 'initial'; + $css .= '--wp--style--global--content-size: ' . $content_size . ';'; + $css .= '--wp--style--global--wide-size: ' . $wide_size . ';'; + } + + $css .= '}'; + + if ( $use_root_padding ) { + // Top and bottom padding are applied to the outer block container. + $css .= '.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }'; + // Right and left padding are applied to the first container with `.has-global-padding` class. + $css .= '.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }'; + // Nested containers with `.has-global-padding` class do not get padding. + $css .= '.has-global-padding :where(.has-global-padding) { padding-right: 0; padding-left: 0; }'; + // Alignfull children of the container with left and right padding have negative margins so they can still be full width. + $css .= '.has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }'; + // The above rule is negated for alignfull children of nested containers. + $css .= '.has-global-padding :where(.has-global-padding) > .alignfull { margin-right: 0; margin-left: 0; }'; + // Some of the children of alignfull blocks without content width should also get padding: text blocks and non-alignfull container blocks. + $css .= '.has-global-padding > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }'; + // The above rule also has to be negated for blocks inside nested `.has-global-padding` blocks. + $css .= '.has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0; }'; + } + + $css .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }'; + $css .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }'; + $css .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; + + $block_gap_value = _wp_array_get( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ), '0.5em' ); + $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null; + if ( $has_block_gap_support ) { + $block_gap_value = static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ) ); + $css .= '.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }'; + $css .= ".wp-site-blocks > * + * { margin-block-start: $block_gap_value; }"; + + // For backwards compatibility, ensure the legacy block gap CSS variable is still available. + $css .= "$selector { --wp--style--block-gap: $block_gap_value; }"; + } + $css .= $this->get_layout_styles( $block_metadata ); + + return $css; + } + + /** + * For metadata values that can either be booleans or paths to booleans, gets the value. + * + * ```php + * $data = array( + * 'color' => array( + * 'defaultPalette' => true + * ) + * ); + * + * static::get_metadata_boolean( $data, false ); + * // => false + * + * static::get_metadata_boolean( $data, array( 'color', 'defaultPalette' ) ); + * // => true + * ``` + * + * @since 6.0.0 + * + * @param array $data The data to inspect. + * @param bool|array $path Boolean or path to a boolean. + * @param bool $default Default value if the referenced path is missing. + * Default false. + * @return bool Value of boolean metadata. + */ + protected static function get_metadata_boolean( $data, $path, $default = false ) { + if ( is_bool( $path ) ) { + return $path; + } + + if ( is_array( $path ) ) { + $value = _wp_array_get( $data, $path ); + if ( null !== $value ) { + return $value; + } + } + + return $default; + } + + /** + * Merges new incoming data. + * + * @since 5.8.0 + * @since 5.9.0 Duotone preset also has origins. + * + * @param WP_Theme_JSON $incoming Data to merge. + */ + public function merge( $incoming ) { + $incoming_data = $incoming->get_raw_data(); + $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data ); + + /* + * The array_replace_recursive algorithm merges at the leaf level, + * but we don't want leaf arrays to be merged, so we overwrite it. + * + * For leaf values that are sequential arrays it will use the numeric indexes for replacement. + * We rather replace the existing with the incoming value, if it exists. + * This is the case of spacing.units. + * + * For leaf values that are associative arrays it will merge them as expected. + * This is also not the behavior we want for the current associative arrays (presets). + * We rather replace the existing with the incoming value, if it exists. + * This happens, for example, when we merge data from theme.json upon existing + * theme supports or when we merge anything coming from the same source twice. + * This is the case of color.palette, color.gradients, color.duotone, + * typography.fontSizes, or typography.fontFamilies. + * + * Additionally, for some preset types, we also want to make sure the + * values they introduce don't conflict with default values. We do so + * by checking the incoming slugs for theme presets and compare them + * with the equivalent default presets: if a slug is present as a default + * we remove it from the theme presets. + */ + $nodes = static::get_setting_nodes( $incoming_data ); + $slugs_global = static::get_default_slugs( $this->theme_json, array( 'settings' ) ); + foreach ( $nodes as $node ) { + // Replace the spacing.units. + $path = $node['path']; + $path[] = 'spacing'; + $path[] = 'units'; + + $content = _wp_array_get( $incoming_data, $path, null ); + if ( isset( $content ) ) { + _wp_array_set( $this->theme_json, $path, $content ); + } + + // Replace the presets. + foreach ( static::PRESETS_METADATA as $preset ) { + $override_preset = ! static::get_metadata_boolean( $this->theme_json['settings'], $preset['prevent_override'], true ); + + foreach ( static::VALID_ORIGINS as $origin ) { + $base_path = $node['path']; + foreach ( $preset['path'] as $leaf ) { + $base_path[] = $leaf; + } + + $path = $base_path; + $path[] = $origin; - if ( $use_root_padding ) { - // Top and bottom padding are applied to the outer block container. - $css .= '.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }'; - // Right and left padding are applied to the first container with `.has-global-padding` class. - $css .= '.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }'; - // Nested containers with `.has-global-padding` class do not get padding. - $css .= '.has-global-padding :where(.has-global-padding) { padding-right: 0; padding-left: 0; }'; - // Alignfull children of the container with left and right padding have negative margins so they can still be full width. - $css .= '.has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }'; - // The above rule is negated for alignfull children of nested containers. - $css .= '.has-global-padding :where(.has-global-padding) > .alignfull { margin-right: 0; margin-left: 0; }'; - // Some of the children of alignfull blocks without content width should also get padding: text blocks and non-alignfull container blocks. - $css .= '.has-global-padding > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }'; - // The above rule also has to be negated for blocks inside nested `.has-global-padding` blocks. - $css .= '.has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0; }'; + $content = _wp_array_get( $incoming_data, $path, null ); + if ( ! isset( $content ) ) { + continue; + } + + if ( 'theme' === $origin && $preset['use_default_names'] ) { + foreach ( $content as $key => $item ) { + if ( ! isset( $item['name'] ) ) { + $name = static::get_name_from_defaults( $item['slug'], $base_path ); + if ( null !== $name ) { + $content[ $key ]['name'] = $name; + } + } + } + } + + if ( + ( 'theme' !== $origin ) || + ( 'theme' === $origin && $override_preset ) + ) { + _wp_array_set( $this->theme_json, $path, $content ); + } else { + $slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] ); + $slugs = array_merge_recursive( $slugs_global, $slugs_node ); + + $slugs_for_preset = _wp_array_get( $slugs, $preset['path'], array() ); + $content = static::filter_slugs( $content, $slugs_for_preset ); + _wp_array_set( $this->theme_json, $path, $content ); + } + } + } } + } - $css .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }'; - $css .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }'; - $css .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; + /** + * Converts all filter (duotone) presets into SVGs. + * + * @since 5.9.1 + * + * @param array $origins List of origins to process. + * @return string SVG filters. + */ + public function get_svg_filters( $origins ) { + $blocks_metadata = static::get_blocks_metadata(); + $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata ); - $block_gap_value = _wp_array_get( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ), '0.5em' ); - $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null; - if ( $has_block_gap_support ) { - $block_gap_value = static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ) ); - $css .= '.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }'; - $css .= ".wp-site-blocks > * + * { margin-block-start: $block_gap_value; }"; + $filters = ''; + foreach ( $setting_nodes as $metadata ) { + $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); + if ( empty( $node['color']['duotone'] ) ) { + continue; + } - // For backwards compatibility, ensure the legacy block gap CSS variable is still available. - $css .= "$selector { --wp--style--block-gap: $block_gap_value; }"; + $duotone_presets = $node['color']['duotone']; + + foreach ( $origins as $origin ) { + if ( ! isset( $duotone_presets[ $origin ] ) ) { + continue; + } + foreach ( $duotone_presets[ $origin ] as $duotone_preset ) { + $filters .= wp_get_duotone_filter_svg( $duotone_preset ); + } + } } - $css .= $this->get_layout_styles( $block_metadata ); - return $css; + return $filters; } /** - * Converts each style section into a list of rulesets - * containing the block styles to be appended to the stylesheet. + * Determines whether a presets should be overridden or not. * - * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax + * @since 5.9.0 + * @deprecated 6.0.0 Use {@see 'get_metadata_boolean'} instead. * - * For each section this creates a new ruleset such as: + * @param array $theme_json The theme.json like structure to inspect. + * @param array $path Path to inspect. + * @param bool|array $override Data to compute whether to override the preset. + * @return boolean + */ + protected static function should_override_preset( $theme_json, $path, $override ) { + _deprecated_function( __METHOD__, '6.0.0', 'get_metadata_boolean' ); + + if ( is_bool( $override ) ) { + return $override; + } + + /* + * The relationship between whether to override the defaults + * and whether the defaults are enabled is inverse: + * + * - If defaults are enabled => theme presets should not be overridden + * - If defaults are disabled => theme presets should be overridden + * + * For example, a theme sets defaultPalette to false, + * making the default palette hidden from the user. + * In that case, we want all the theme presets to be present, + * so they should override the defaults. + */ + if ( is_array( $override ) ) { + $value = _wp_array_get( $theme_json, array_merge( $path, $override ) ); + if ( isset( $value ) ) { + return ! $value; + } + + // Search the top-level key if none was found for this node. + $value = _wp_array_get( $theme_json, array_merge( array( 'settings' ), $override ) ); + if ( isset( $value ) ) { + return ! $value; + } + + return true; + } + } + + /** + * Returns the default slugs for all the presets in an associative array + * whose keys are the preset paths and the leafs is the list of slugs. * - * block-selector { - * style-property-one: value; - * } + * For example: * - * @param array $style_nodes Nodes with styles. - * @return string The new stylesheet. + * array( + * 'color' => array( + * 'palette' => array( 'slug-1', 'slug-2' ), + * 'gradients' => array( 'slug-3', 'slug-4' ), + * ), + * ) + * + * @since 5.9.0 + * + * @param array $data A theme.json like structure. + * @param array $node_path The path to inspect. It's 'settings' by default. + * @return array */ - protected function get_block_classes( $style_nodes ) { - $block_rules = ''; + protected static function get_default_slugs( $data, $node_path ) { + $slugs = array(); - foreach ( $style_nodes as $metadata ) { - if ( null === $metadata['selector'] ) { + foreach ( static::PRESETS_METADATA as $metadata ) { + $path = $node_path; + foreach ( $metadata['path'] as $leaf ) { + $path[] = $leaf; + } + $path[] = 'default'; + + $preset = _wp_array_get( $data, $path, null ); + if ( ! isset( $preset ) ) { continue; } - $block_rules .= static::get_styles_for_block( $metadata ); + + $slugs_for_preset = array(); + foreach ( $preset as $item ) { + if ( isset( $item['slug'] ) ) { + $slugs_for_preset[] = $item['slug']; + } + } + + _wp_array_set( $slugs, $metadata['path'], $slugs_for_preset ); } - return $block_rules; + return $slugs; } /** - * Given a styles array, it extracts the style properties - * and adds them to the $declarations array following the format: + * Gets a `default`'s preset name by a provided slug. * - * ```php - * array( - * 'name' => 'property_name', - * 'value' => 'property_value, - * ) - * ``` + * @since 5.9.0 * - * @param array $styles Styles to process. - * @param array $settings Theme settings. - * @param array $properties Properties metadata. - * @param array $theme_json Theme JSON array. - * @param string $selector The style block selector. - * @param boolean $use_root_padding Whether to add custom properties at root level. - * @return array Returns the modified $declarations. + * @param string $slug The slug we want to find a match from default presets. + * @param array $base_path The path to inspect. It's 'settings' by default. + * @return string|null */ - protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) { - if ( null === $properties ) { - $properties = static::PROPERTIES_METADATA; + protected function get_name_from_defaults( $slug, $base_path ) { + $path = $base_path; + $path[] = 'default'; + $default_content = _wp_array_get( $this->theme_json, $path, null ); + if ( ! $default_content ) { + return null; + } + foreach ( $default_content as $item ) { + if ( $slug === $item['slug'] ) { + return $item['name']; + } } + return null; + } - $declarations = array(); - if ( empty( $styles ) ) { - return $declarations; + /** + * Removes the preset values whose slug is equal to any of given slugs. + * + * @since 5.9.0 + * + * @param array $node The node with the presets to validate. + * @param array $slugs The slugs that should not be overridden. + * @return array The new node. + */ + protected static function filter_slugs( $node, $slugs ) { + if ( empty( $slugs ) ) { + return $node; } - $root_variable_duplicates = array(); + $new_node = array(); + foreach ( $node as $value ) { + if ( isset( $value['slug'] ) && ! in_array( $value['slug'], $slugs, true ) ) { + $new_node[] = $value; + } + } - foreach ( $properties as $css_property => $value_path ) { - $value = static::get_property_value( $styles, $value_path, $theme_json ); + return $new_node; + } - if ( str_starts_with( $css_property, '--wp--style--root--' ) && ( static::ROOT_BLOCK_SELECTOR !== $selector || ! $use_root_padding ) ) { + /** + * Removes insecure data from theme.json. + * + * @since 5.9.0 + * + * @param array $theme_json Structure to sanitize. + * @return array Sanitized structure. + */ + public static function remove_insecure_properties( $theme_json ) { + $sanitized = array(); + + $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); + + $valid_block_names = array_keys( static::get_blocks_metadata() ); + $valid_element_names = array_keys( static::ELEMENTS ); + + $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names ); + + $blocks_metadata = static::get_blocks_metadata(); + $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata ); + + foreach ( $style_nodes as $metadata ) { + $input = _wp_array_get( $theme_json, $metadata['path'], array() ); + if ( empty( $input ) ) { continue; } - // Root-level padding styles don't currently support strings with CSS shorthand values. - // This may change: https://github.com/WordPress/gutenberg/issues/40132. - if ( '--wp--style--root--padding' === $css_property && is_string( $value ) ) { + + $output = static::remove_insecure_styles( $input ); + + /* + * Get a reference to element name from path. + * $metadata['path'] = array( 'styles', 'elements', 'link' ); + */ + $current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ]; + + /* + * $output is stripped of pseudo selectors. Re-add and process them + * or insecure styles here. + */ + // TODO: Replace array_key_exists() with isset() check once WordPress drops + // support for PHP 5.6. See https://core.trac.wordpress.org/ticket/57067. + if ( array_key_exists( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) { + if ( isset( $input[ $pseudo_selector ] ) ) { + $output[ $pseudo_selector ] = static::remove_insecure_styles( $input[ $pseudo_selector ] ); + } + } + } + + if ( ! empty( $output ) ) { + _wp_array_set( $sanitized, $metadata['path'], $output ); + } + } + + $setting_nodes = static::get_setting_nodes( $theme_json ); + foreach ( $setting_nodes as $metadata ) { + $input = _wp_array_get( $theme_json, $metadata['path'], array() ); + if ( empty( $input ) ) { continue; } - if ( str_starts_with( $css_property, '--wp--style--root--' ) && $use_root_padding ) { - $root_variable_duplicates[] = substr( $css_property, strlen( '--wp--style--root--' ) ); + $output = static::remove_insecure_settings( $input ); + if ( ! empty( $output ) ) { + _wp_array_set( $sanitized, $metadata['path'], $output ); } + } - // Look up protected properties, keyed by value path. - // Skip protected properties that are explicitly set to `null`. - if ( is_array( $value_path ) ) { - $path_string = implode( '.', $value_path ); - if ( - array_key_exists( $path_string, static::PROTECTED_PROPERTIES ) && - _wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null - ) { + if ( empty( $sanitized['styles'] ) ) { + unset( $theme_json['styles'] ); + } else { + $theme_json['styles'] = $sanitized['styles']; + } + + if ( empty( $sanitized['settings'] ) ) { + unset( $theme_json['settings'] ); + } else { + $theme_json['settings'] = $sanitized['settings']; + } + + return $theme_json; + } + + /** + * Processes a setting node and returns the same node + * without the insecure settings. + * + * @since 5.9.0 + * + * @param array $input Node to process. + * @return array + */ + protected static function remove_insecure_settings( $input ) { + $output = array(); + foreach ( static::PRESETS_METADATA as $preset_metadata ) { + foreach ( static::VALID_ORIGINS as $origin ) { + $path_with_origin = $preset_metadata['path']; + $path_with_origin[] = $origin; + $presets = _wp_array_get( $input, $path_with_origin, null ); + if ( null === $presets ) { continue; } + + $escaped_preset = array(); + foreach ( $presets as $preset ) { + if ( + esc_attr( esc_html( $preset['name'] ) ) === $preset['name'] && + sanitize_html_class( $preset['slug'] ) === $preset['slug'] + ) { + $value = null; + if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) { + $value = $preset[ $preset_metadata['value_key'] ]; + } elseif ( + isset( $preset_metadata['value_func'] ) && + is_callable( $preset_metadata['value_func'] ) + ) { + $value = call_user_func( $preset_metadata['value_func'], $preset ); + } + + $preset_is_valid = true; + foreach ( $preset_metadata['properties'] as $property ) { + if ( ! static::is_safe_css_declaration( $property, $value ) ) { + $preset_is_valid = false; + break; + } + } + + if ( $preset_is_valid ) { + $escaped_preset[] = $preset; + } + } + } + + if ( ! empty( $escaped_preset ) ) { + _wp_array_set( $output, $path_with_origin, $escaped_preset ); + } } + } - // Skip if empty and not "0" or value represents array of longhand values. - $has_missing_value = empty( $value ) && ! is_numeric( $value ); - if ( $has_missing_value || is_array( $value ) ) { - continue; + foreach ( static::INDIRECT_PROPERTIES_METADATA as $property => $paths ) { + foreach ( $paths as $path ) { + $value = _wp_array_get( $input, $path, array() ); + if ( + isset( $value ) && + ! is_array( $value ) && + static::is_safe_css_declaration( $property, $value ) + ) { + _wp_array_set( $output, $path, $value ); + } } + } + return $output; + } - // Calculates fluid typography rules where available. - if ( 'font-size' === $css_property ) { - /* - * gutenberg_get_typography_font_size_value() will check - * if fluid typography has been activated and also - * whether the incoming value can be converted to a fluid value. - * Values that already have a "clamp()" function will not pass the test, - * and therefore the original $value will be returned. - */ - $value = gutenberg_get_typography_font_size_value( array( 'size' => $value ) ); + /** + * Processes a style node and returns the same node + * without the insecure styles. + * + * @since 5.9.0 + * @since 6.2.0 Allow indirect properties used outside of `compute_style_properties`. + * + * @param array $input Node to process. + * @return array + */ + protected static function remove_insecure_styles( $input ) { + $output = array(); + $declarations = static::compute_style_properties( $input ); + + foreach ( $declarations as $declaration ) { + if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) { + $path = static::PROPERTIES_METADATA[ $declaration['name'] ]; + + // Check the value isn't an array before adding so as to not + // double up shorthand and longhand styles. + $value = _wp_array_get( $input, $path, array() ); + if ( ! is_array( $value ) ) { + _wp_array_set( $output, $path, $value ); + } } - - $declarations[] = array( - 'name' => $css_property, - 'value' => $value, - ); } - // If a variable value is added to the root, the corresponding property should be removed. - foreach ( $root_variable_duplicates as $duplicate ) { - $discard = array_search( $duplicate, array_column( $declarations, 'name' ), true ); - if ( is_numeric( $discard ) ) { - array_splice( $declarations, $discard, 1 ); + // Ensure indirect properties not handled by `compute_style_properties` are allowed. + foreach ( static::INDIRECT_PROPERTIES_METADATA as $property => $paths ) { + foreach ( $paths as $path ) { + $value = _wp_array_get( $input, $path, array() ); + if ( + isset( $value ) && + ! is_array( $value ) && + static::is_safe_css_declaration( $property, $value ) + ) { + _wp_array_set( $output, $path, $value ); + } } } - return $declarations; + return $output; } /** - * Returns the style property for the given path. + * Checks that a declaration provided by the user is safe. * - * It also converts CSS Custom Property stored as - * "var:preset|color|secondary" to the form - * "--wp--preset--color--secondary". + * @since 5.9.0 * - * It also converts references to a path to the value - * stored at that location, e.g. - * { "ref": "style.color.background" } => "#fff". + * @param string $property_name Property name in a CSS declaration, i.e. the `color` in `color: red`. + * @param string $property_value Value in a CSS declaration, i.e. the `red` in `color: red`. + * @return bool + */ + protected static function is_safe_css_declaration( $property_name, $property_value ) { + $style_to_validate = $property_name . ': ' . $property_value; + $filtered = esc_html( safecss_filter_attr( $style_to_validate ) ); + return ! empty( trim( $filtered ) ); + } + + /** + * Returns the raw data. * - * @param array $styles Styles subtree. - * @param array $path Which property to process. - * @param array $theme_json Theme JSON array. - * @return string|array|null Style property value. + * @since 5.8.0 + * + * @return array Raw data. */ - protected static function get_property_value( $styles, $path, $theme_json = null ) { - $value = _wp_array_get( $styles, $path ); + public function get_raw_data() { + return $this->theme_json; + } - // This converts references to a path to the value at that path - // where the values is an array with a "ref" key, pointing to a path. - // For example: { "ref": "style.color.background" } => "#fff". - if ( is_array( $value ) && array_key_exists( 'ref', $value ) ) { - $value_path = explode( '.', $value['ref'] ); - $ref_value = _wp_array_get( $theme_json, $value_path ); - // Only use the ref value if we find anything. - if ( ! empty( $ref_value ) && is_string( $ref_value ) ) { - $value = $ref_value; + /** + * Transforms the given editor settings according the + * add_theme_support format to the theme.json format. + * + * @since 5.8.0 + * + * @param array $settings Existing editor settings. + * @return array Config that adheres to the theme.json schema. + */ + public static function get_from_editor_settings( $settings ) { + $theme_settings = array( + 'version' => static::LATEST_SCHEMA, + 'settings' => array(), + ); + + // Deprecated theme supports. + if ( isset( $settings['disableCustomColors'] ) ) { + if ( ! isset( $theme_settings['settings']['color'] ) ) { + $theme_settings['settings']['color'] = array(); } + $theme_settings['settings']['color']['custom'] = ! $settings['disableCustomColors']; + } - if ( is_array( $ref_value ) && array_key_exists( 'ref', $ref_value ) ) { - $path_string = json_encode( $path ); - $ref_value_string = json_encode( $ref_value ); - _doing_it_wrong( 'get_property_value', "Your theme.json file uses a dynamic value ({$ref_value_string}) for the path at {$path_string}. However, the value at {$path_string} is also a dynamic value (pointing to {$ref_value['ref']}) and pointing to another dynamic value is not supported. Please update {$path_string} to point directly to {$ref_value['ref']}.", '6.1.0' ); + if ( isset( $settings['disableCustomGradients'] ) ) { + if ( ! isset( $theme_settings['settings']['color'] ) ) { + $theme_settings['settings']['color'] = array(); } + $theme_settings['settings']['color']['customGradient'] = ! $settings['disableCustomGradients']; } - if ( ! $value || is_array( $value ) ) { - return $value; + if ( isset( $settings['disableCustomFontSizes'] ) ) { + if ( ! isset( $theme_settings['settings']['typography'] ) ) { + $theme_settings['settings']['typography'] = array(); + } + $theme_settings['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes']; } - // Convert custom CSS properties. - $prefix = 'var:'; - $prefix_len = strlen( $prefix ); - $token_in = '|'; - $token_out = '--'; - if ( 0 === strncmp( $value, $prefix, $prefix_len ) ) { - $unwrapped_name = str_replace( - $token_in, - $token_out, - substr( $value, $prefix_len ) - ); - $value = "var(--wp--$unwrapped_name)"; + if ( isset( $settings['enableCustomLineHeight'] ) ) { + if ( ! isset( $theme_settings['settings']['typography'] ) ) { + $theme_settings['settings']['typography'] = array(); + } + $theme_settings['settings']['typography']['lineHeight'] = $settings['enableCustomLineHeight']; } - return $value; + if ( isset( $settings['enableCustomUnits'] ) ) { + if ( ! isset( $theme_settings['settings']['spacing'] ) ) { + $theme_settings['settings']['spacing'] = array(); + } + $theme_settings['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ? + array( 'px', 'em', 'rem', 'vh', 'vw', '%' ) : + $settings['enableCustomUnits']; + } + + if ( isset( $settings['colors'] ) ) { + if ( ! isset( $theme_settings['settings']['color'] ) ) { + $theme_settings['settings']['color'] = array(); + } + $theme_settings['settings']['color']['palette'] = $settings['colors']; + } + + if ( isset( $settings['gradients'] ) ) { + if ( ! isset( $theme_settings['settings']['color'] ) ) { + $theme_settings['settings']['color'] = array(); + } + $theme_settings['settings']['color']['gradients'] = $settings['gradients']; + } + + if ( isset( $settings['fontSizes'] ) ) { + $font_sizes = $settings['fontSizes']; + // Back-compatibility for presets without units. + foreach ( $font_sizes as $key => $font_size ) { + if ( is_numeric( $font_size['size'] ) ) { + $font_sizes[ $key ]['size'] = $font_size['size'] . 'px'; + } + } + if ( ! isset( $theme_settings['settings']['typography'] ) ) { + $theme_settings['settings']['typography'] = array(); + } + $theme_settings['settings']['typography']['fontSizes'] = $font_sizes; + } + + if ( isset( $settings['enableCustomSpacing'] ) ) { + if ( ! isset( $theme_settings['settings']['spacing'] ) ) { + $theme_settings['settings']['spacing'] = array(); + } + $theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing']; + } + + return $theme_settings; } /** - * Presets are a set of values that serve - * to bootstrap some styles: colors, font sizes, etc. - * - * They are a unkeyed array of values such as: - * - * ```php - * array( - * array( - * 'slug' => 'unique-name-within-the-set', - * 'name' => 'Name for the UI', - * => 'value' - * ), - * ) - * ``` + * Returns the current theme's wanted patterns(slugs) to be + * registered from Pattern Directory. * - * This contains the necessary metadata to process them: + * @since 6.0.0 * - * - path => Where to find the preset within the settings section. - * - prevent_override => Disables override of default presets by theme presets. - * The relationship between whether to override the defaults - * and whether the defaults are enabled is inverse: - * - If defaults are enabled => theme presets should not be overridden - * - If defaults are disabled => theme presets should be overridden - * For example, a theme sets defaultPalette to false, - * making the default palette hidden from the user. - * In that case, we want all the theme presets to be present, - * so they should override the defaults by setting this false. - * - use_default_names => whether to use the default names - * - value_key => the key that represents the value - * - value_func => optionally, instead of value_key, a function to generate - * the value that takes a preset as an argument - * (either value_key or value_func should be present) - * - css_vars => template string to use in generating the CSS Custom Property. - * Example output: "--wp--preset--duotone--blue: " will generate as many CSS Custom Properties as presets defined - * substituting the $slug for the slug's value for each preset value. - * - classes => array containing a structure with the classes to - * generate for the presets, where for each array item - * the key is the class name and the value the property name. - * The "$slug" substring will be replaced by the slug of each preset. - * For example: - * 'classes' => array( - * '.has-$slug-color' => 'color', - * '.has-$slug-background-color' => 'background-color', - * '.has-$slug-border-color' => 'border-color', - * ) - * - properties => array of CSS properties to be used by kses to - * validate the content of each preset - * by means of the remove_insecure_properties method. + * @return string[] */ - const PRESETS_METADATA = array( - array( - 'path' => array( 'color', 'palette' ), - 'prevent_override' => array( 'color', 'defaultPalette' ), - 'use_default_names' => false, - 'value_key' => 'color', - 'css_vars' => '--wp--preset--color--$slug', - 'classes' => array( - '.has-$slug-color' => 'color', - '.has-$slug-background-color' => 'background-color', - '.has-$slug-border-color' => 'border-color', - ), - 'properties' => array( 'color', 'background-color', 'border-color' ), - ), - array( - 'path' => array( 'color', 'gradients' ), - 'prevent_override' => array( 'color', 'defaultGradients' ), - 'use_default_names' => false, - 'value_key' => 'gradient', - 'css_vars' => '--wp--preset--gradient--$slug', - 'classes' => array( '.has-$slug-gradient-background' => 'background' ), - 'properties' => array( 'background' ), - ), - array( - 'path' => array( 'color', 'duotone' ), - 'prevent_override' => array( 'color', 'defaultDuotone' ), - 'use_default_names' => false, - 'value_func' => 'gutenberg_get_duotone_filter_property', - 'css_vars' => '--wp--preset--duotone--$slug', - 'classes' => array(), - 'properties' => array( 'filter' ), - ), - array( - 'path' => array( 'typography', 'fontSizes' ), - 'prevent_override' => false, - 'use_default_names' => true, - 'value_func' => 'gutenberg_get_typography_font_size_value', - 'css_vars' => '--wp--preset--font-size--$slug', - 'classes' => array( '.has-$slug-font-size' => 'font-size' ), - 'properties' => array( 'font-size' ), - ), - array( - 'path' => array( 'typography', 'fontFamilies' ), - 'prevent_override' => false, - 'use_default_names' => false, - 'value_key' => 'fontFamily', - 'css_vars' => '--wp--preset--font-family--$slug', - 'classes' => array( '.has-$slug-font-family' => 'font-family' ), - 'properties' => array( 'font-family' ), - ), - array( - 'path' => array( 'spacing', 'spacingSizes' ), - 'prevent_override' => false, - 'use_default_names' => true, - 'value_key' => 'size', - 'css_vars' => '--wp--preset--spacing--$slug', - 'classes' => array(), - 'properties' => array( 'padding', 'margin' ), - ), - ); + public function get_patterns() { + if ( isset( $this->theme_json['patterns'] ) && is_array( $this->theme_json['patterns'] ) ) { + return $this->theme_json['patterns']; + } + return array(); + } /** - * The valid properties under the settings key. + * Returns a valid theme.json as provided by a theme. * - * @var array + * Unlike get_raw_data() this returns the presets flattened, as provided by a theme. + * This also uses appearanceTools instead of their opt-ins if all of them are true. + * + * @since 6.0.0 + * + * @return array */ - const VALID_SETTINGS = array( - 'appearanceTools' => null, - 'useRootPaddingAwareAlignments' => null, - 'border' => array( - 'color' => null, - 'radius' => null, - 'style' => null, - 'width' => null, - ), - 'color' => array( - 'background' => null, - 'custom' => null, - 'customDuotone' => null, - 'customGradient' => null, - 'defaultDuotone' => null, - 'defaultGradients' => null, - 'defaultPalette' => null, - 'duotone' => null, - 'gradients' => null, - 'link' => null, - 'palette' => null, - 'text' => null, - ), - 'custom' => null, - 'layout' => array( - 'contentSize' => null, - 'definitions' => null, - 'wideSize' => null, - ), - 'spacing' => array( - 'customSpacingSize' => null, - 'spacingSizes' => null, - 'spacingScale' => null, - 'blockGap' => null, - 'margin' => null, - 'padding' => null, - 'units' => null, - ), - 'typography' => array( - 'fluid' => null, - 'customFontSize' => null, - 'dropCap' => null, - 'fontFamilies' => null, - 'fontSizes' => null, - 'fontStyle' => null, - 'fontWeight' => null, - 'letterSpacing' => null, - 'lineHeight' => null, - 'textDecoration' => null, - 'textTransform' => null, - ), - ); + public function get_data() { + $output = $this->theme_json; + $nodes = static::get_setting_nodes( $output ); + + /** + * Flatten the theme & custom origins into a single one. + * + * For example, the following: + * + * { + * "settings": { + * "color": { + * "palette": { + * "theme": [ {} ], + * "custom": [ {} ] + * } + * } + * } + * } + * + * will be converted to: + * + * { + * "settings": { + * "color": { + * "palette": [ {} ] + * } + * } + * } + */ + foreach ( $nodes as $node ) { + foreach ( static::PRESETS_METADATA as $preset_metadata ) { + $path = $node['path']; + foreach ( $preset_metadata['path'] as $preset_metadata_path ) { + $path[] = $preset_metadata_path; + } + $preset = _wp_array_get( $output, $path, null ); + if ( null === $preset ) { + continue; + } + + $items = array(); + if ( isset( $preset['theme'] ) ) { + foreach ( $preset['theme'] as $item ) { + $slug = $item['slug']; + unset( $item['slug'] ); + $items[ $slug ] = $item; + } + } + if ( isset( $preset['custom'] ) ) { + foreach ( $preset['custom'] as $item ) { + $slug = $item['slug']; + unset( $item['slug'] ); + $items[ $slug ] = $item; + } + } + $flattened_preset = array(); + foreach ( $items as $slug => $value ) { + $flattened_preset[] = array_merge( array( 'slug' => (string) $slug ), $value ); + } + _wp_array_set( $output, $path, $flattened_preset ); + } + } + + // If all of the static::APPEARANCE_TOOLS_OPT_INS are true, + // this code unsets them and sets 'appearanceTools' instead. + foreach ( $nodes as $node ) { + $all_opt_ins_are_set = true; + foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) { + $full_path = $node['path']; + foreach ( $opt_in_path as $opt_in_path_item ) { + $full_path[] = $opt_in_path_item; + } + // Use "unset prop" as a marker instead of "null" because + // "null" can be a valid value for some props (e.g. blockGap). + $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' ); + if ( 'unset prop' === $opt_in_value ) { + $all_opt_ins_are_set = false; + break; + } + } + + if ( $all_opt_ins_are_set ) { + $node_path_with_appearance_tools = $node['path']; + $node_path_with_appearance_tools[] = 'appearanceTools'; + _wp_array_set( $output, $node_path_with_appearance_tools, true ); + foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) { + $full_path = $node['path']; + foreach ( $opt_in_path as $opt_in_path_item ) { + $full_path[] = $opt_in_path_item; + } + // Use "unset prop" as a marker instead of "null" because + // "null" can be a valid value for some props (e.g. blockGap). + $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' ); + if ( true !== $opt_in_value ) { + continue; + } + + // The following could be improved to be path independent. + // At the moment it relies on a couple of assumptions: + // + // - all opt-ins having a path of size 2. + // - there's two sources of settings: the top-level and the block-level. + if ( + ( 1 === count( $node['path'] ) ) && + ( 'settings' === $node['path'][0] ) + ) { + // Top-level settings. + unset( $output['settings'][ $opt_in_path[0] ][ $opt_in_path[1] ] ); + if ( empty( $output['settings'][ $opt_in_path[0] ] ) ) { + unset( $output['settings'][ $opt_in_path[0] ] ); + } + } elseif ( + ( 3 === count( $node['path'] ) ) && + ( 'settings' === $node['path'][0] ) && + ( 'blocks' === $node['path'][1] ) + ) { + // Block-level settings. + $block_name = $node['path'][2]; + unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ][ $opt_in_path[1] ] ); + if ( empty( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ) ) { + unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ); + } + } + } + } + } + + wp_recursive_ksort( $output ); + + return $output; + } /** - * Transform the spacing scale values into an array of spacing scale presets. + * Sets the spacingSizes array based on the spacingScale values from theme.json. + * + * @since 6.1.0 + * + * @return null|void */ public function set_spacing_sizes() { $spacing_scale = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'spacingScale' ), array() ); - if ( ! is_numeric( $spacing_scale['steps'] ) + // Gutenberg didn't have the 1st isset check. + if ( ! isset( $spacing_scale['steps'] ) + || ! is_numeric( $spacing_scale['steps'] ) || ! isset( $spacing_scale['mediumStep'] ) || ! isset( $spacing_scale['unit'] ) || ! isset( $spacing_scale['operator'] ) @@ -1410,7 +3235,7 @@ public function set_spacing_sizes() { $below_sizes[] = array( /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Small. */ - 'name' => $below_midpoint_count === $steps_mid_point - 1 ? __( 'Small', 'gutenberg' ) : sprintf( __( '%sX-Small', 'gutenberg' ), strval( $x_small_count ) ), + 'name' => $below_midpoint_count === $steps_mid_point - 1 ? __( 'Small', 'gutenberg' ) : sprintf( __( '%sX-Small', 'gutenberg' ), (string) $x_small_count ), 'slug' => (string) $slug, 'size' => round( $current_step, 2 ) . $unit, ); @@ -1420,7 +3245,7 @@ public function set_spacing_sizes() { } if ( $below_midpoint_count < $steps_mid_point - 2 ) { - ++$x_small_count; + $x_small_count++; } $slug -= 10; @@ -1447,7 +3272,7 @@ public function set_spacing_sizes() { $above_sizes[] = array( /* translators: %s: Digit to indicate multiple of sizing, eg. 2X-Large. */ - 'name' => 0 === $above_midpoint_count ? __( 'Large', 'gutenberg' ) : sprintf( __( '%sX-Large', 'gutenberg' ), strval( $x_large_count ) ), + 'name' => 0 === $above_midpoint_count ? __( 'Large', 'gutenberg' ) : sprintf( __( '%sX-Large', 'gutenberg' ), (string) $x_large_count ), 'slug' => (string) $slug, 'size' => round( $current_step, 2 ) . $unit, ); @@ -1457,244 +3282,24 @@ public function set_spacing_sizes() { } if ( $above_midpoint_count > 1 ) { - ++$x_large_count; + $x_large_count++; } $slug += 10; } - $spacing_sizes = array_merge( $below_sizes, $above_sizes ); + $spacing_sizes = $below_sizes; + foreach ( $above_sizes as $above_sizes_item ) { + $spacing_sizes[] = $above_sizes_item; + } // If there are 7 or less steps in the scale revert to numbers for labels instead of t-shirt sizes. if ( $spacing_scale['steps'] <= 7 ) { for ( $spacing_sizes_count = 0; $spacing_sizes_count < count( $spacing_sizes ); $spacing_sizes_count++ ) { - $spacing_sizes[ $spacing_sizes_count ]['name'] = strval( $spacing_sizes_count + 1 ); + $spacing_sizes[ $spacing_sizes_count ]['name'] = (string) ( $spacing_sizes_count + 1 ); } } _wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), $spacing_sizes ); } - - /** - * Get the CSS layout rules for a particular block from theme.json layout definitions. - * - * @param array $block_metadata Metadata about the block to get styles for. - * - * @return string Layout styles for the block. - */ - protected function get_layout_styles( $block_metadata ) { - $block_rules = ''; - $block_type = null; - - // Skip outputting layout styles if explicitly disabled. - if ( current_theme_supports( 'disable-layout-styles' ) ) { - return $block_rules; - } - - if ( isset( $block_metadata['name'] ) ) { - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_metadata['name'] ); - if ( ! block_has_support( $block_type, array( '__experimentalLayout' ), false ) ) { - return $block_rules; - } - } - - $selector = isset( $block_metadata['selector'] ) ? $block_metadata['selector'] : ''; - $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null; - $has_fallback_gap_support = ! $has_block_gap_support; // This setting isn't useful yet: it exists as a placeholder for a future explicit fallback gap styles support. - $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); - $layout_definitions = _wp_array_get( $this->theme_json, array( 'settings', 'layout', 'definitions' ), array() ); - $layout_selector_pattern = '/^[a-zA-Z0-9\-\.\ *+>:\(\)]*$/'; // Allow alphanumeric classnames, spaces, wildcard, sibling, child combinator and pseudo class selectors. - - // Gap styles will only be output if the theme has block gap support, or supports a fallback gap. - // Default layout gap styles will be skipped for themes that do not explicitly opt-in to blockGap with a `true` or `false` value. - if ( $has_block_gap_support || $has_fallback_gap_support ) { - $block_gap_value = null; - // Use a fallback gap value if block gap support is not available. - if ( ! $has_block_gap_support ) { - $block_gap_value = static::ROOT_BLOCK_SELECTOR === $selector ? '0.5em' : null; - if ( ! empty( $block_type ) ) { - $block_gap_value = _wp_array_get( $block_type->supports, array( 'spacing', 'blockGap', '__experimentalDefault' ), null ); - } - } else { - $block_gap_value = static::get_property_value( $node, array( 'spacing', 'blockGap' ) ); - } - - // Support split row / column values and concatenate to a shorthand value. - if ( is_array( $block_gap_value ) ) { - if ( isset( $block_gap_value['top'] ) && isset( $block_gap_value['left'] ) ) { - $gap_row = static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) ); - $gap_column = static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) ); - $block_gap_value = $gap_row === $gap_column ? $gap_row : $gap_row . ' ' . $gap_column; - } else { - // Skip outputting gap value if not all sides are provided. - $block_gap_value = null; - } - } - - // If the block should have custom gap, add the gap styles. - if ( null !== $block_gap_value && false !== $block_gap_value && '' !== $block_gap_value ) { - foreach ( $layout_definitions as $layout_definition_key => $layout_definition ) { - // Allow outputting fallback gap styles for flex layout type when block gap support isn't available. - if ( ! $has_block_gap_support && 'flex' !== $layout_definition_key ) { - continue; - } - - $class_name = sanitize_title( _wp_array_get( $layout_definition, array( 'className' ), false ) ); - $spacing_rules = _wp_array_get( $layout_definition, array( 'spacingStyles' ), array() ); - - if ( - ! empty( $class_name ) && - ! empty( $spacing_rules ) - ) { - foreach ( $spacing_rules as $spacing_rule ) { - $declarations = array(); - if ( - isset( $spacing_rule['selector'] ) && - preg_match( $layout_selector_pattern, $spacing_rule['selector'] ) && - ! empty( $spacing_rule['rules'] ) - ) { - // Iterate over each of the styling rules and substitute non-string values such as `null` with the real `blockGap` value. - foreach ( $spacing_rule['rules'] as $css_property => $css_value ) { - $current_css_value = is_string( $css_value ) ? $css_value : $block_gap_value; - if ( static::is_safe_css_declaration( $css_property, $current_css_value ) ) { - $declarations[] = array( - 'name' => $css_property, - 'value' => $current_css_value, - ); - } - } - - if ( ! $has_block_gap_support ) { - // For fallback gap styles, use lower specificity, to ensure styles do not unintentionally override theme styles. - $format = static::ROOT_BLOCK_SELECTOR === $selector ? ':where(.%2$s%3$s)' : ':where(%1$s.%2$s%3$s)'; - $layout_selector = sprintf( - $format, - $selector, - $class_name, - $spacing_rule['selector'] - ); - } else { - $format = static::ROOT_BLOCK_SELECTOR === $selector ? '%s .%s%s' : '%s.%s%s'; - $layout_selector = sprintf( - $format, - $selector, - $class_name, - $spacing_rule['selector'] - ); - } - $block_rules .= static::to_ruleset( $layout_selector, $declarations ); - } - } - } - } - } - } - - // Output base styles. - if ( - static::ROOT_BLOCK_SELECTOR === $selector - ) { - $valid_display_modes = array( 'block', 'flex', 'grid' ); - foreach ( $layout_definitions as $layout_definition ) { - $class_name = sanitize_title( _wp_array_get( $layout_definition, array( 'className' ), false ) ); - $base_style_rules = _wp_array_get( $layout_definition, array( 'baseStyles' ), array() ); - - if ( - ! empty( $class_name ) && - ! empty( $base_style_rules ) - ) { - // Output display mode. This requires special handling as `display` is not exposed in `safe_style_css_filter`. - if ( - ! empty( $layout_definition['displayMode'] ) && - is_string( $layout_definition['displayMode'] ) && - in_array( $layout_definition['displayMode'], $valid_display_modes, true ) - ) { - $layout_selector = sprintf( - '%s .%s', - $selector, - $class_name - ); - $block_rules .= static::to_ruleset( - $layout_selector, - array( - array( - 'name' => 'display', - 'value' => $layout_definition['displayMode'], - ), - ) - ); - } - - foreach ( $base_style_rules as $base_style_rule ) { - $declarations = array(); - - if ( - isset( $base_style_rule['selector'] ) && - preg_match( $layout_selector_pattern, $base_style_rule['selector'] ) && - ! empty( $base_style_rule['rules'] ) - ) { - foreach ( $base_style_rule['rules'] as $css_property => $css_value ) { - if ( static::is_safe_css_declaration( $css_property, $css_value ) ) { - $declarations[] = array( - 'name' => $css_property, - 'value' => $css_value, - ); - } - } - - $layout_selector = sprintf( - '%s .%s%s', - $selector, - $class_name, - $base_style_rule['selector'] - ); - $block_rules .= static::to_ruleset( $layout_selector, $declarations ); - } - } - } - } - } - return $block_rules; - } - - /** - * Function that scopes a selector with another one. This works a bit like - * SCSS nesting except the `&` operator isn't supported. - * - * - * $scope = '.a, .b .c'; - * $selector = '> .x, .y'; - * $merged = scope_selector( $scope, $selector ); - * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y' - * - * - * @since 5.9.0 - * - * @param string $scope Selector to scope to. - * @param string $selector Original selector. - * @return string Scoped selector. - */ - public static function scope_selector( $scope, $selector ) { - $scopes = explode( ',', $scope ); - $selectors = explode( ',', $selector ); - - $selectors_scoped = array(); - foreach ( $scopes as $outer ) { - foreach ( $selectors as $inner ) { - $outer = trim( $outer ); - $inner = trim( $inner ); - if ( ! empty( $outer ) && ! empty( $inner ) ) { - $selectors_scoped[] = $outer . ' ' . $inner; - } elseif ( empty( $outer ) ) { - $selectors_scoped[] = $inner; - } elseif ( empty( $inner ) ) { - $selectors_scoped[] = $outer; - } - } - } - - $result = implode( ', ', $selectors_scoped ); - return $result; - } - } diff --git a/lib/class-wp-theme-json-resolver-gutenberg.php b/lib/class-wp-theme-json-resolver-gutenberg.php new file mode 100644 index 00000000000000..8733fd13bf2c77 --- /dev/null +++ b/lib/class-wp-theme-json-resolver-gutenberg.php @@ -0,0 +1,701 @@ + array(), + 'blocks' => array(), + 'theme' => array(), + 'user' => array(), + ); + + /** + * Container for data coming from core. + * + * @since 5.8.0 + * @var WP_Theme_JSON + */ + protected static $core = null; + + /** + * Container for data coming from the blocks. + * + * @since 6.1.0 + * @var WP_Theme_JSON + */ + protected static $blocks = null; + + /** + * Container for data coming from the theme. + * + * @since 5.8.0 + * @var WP_Theme_JSON + */ + protected static $theme = null; + + /** + * Container for data coming from the user. + * + * @since 5.9.0 + * @var WP_Theme_JSON + */ + protected static $user = null; + + /** + * Stores the ID of the custom post type + * that holds the user data. + * + * @since 5.9.0 + * @var int + */ + protected static $user_custom_post_type_id = null; + + /** + * Container to keep loaded i18n schema for `theme.json`. + * + * @since 5.8.0 As `$theme_json_i18n`. + * @since 5.9.0 Renamed from `$theme_json_i18n` to `$i18n_schema`. + * @var array + */ + protected static $i18n_schema = null; + + /** + * `theme.json` file cache. + * + * @since 6.1.0 + * @var array + */ + protected static $theme_json_file_cache = array(); + + /** + * Processes a file that adheres to the theme.json schema + * and returns an array with its contents, or a void array if none found. + * + * @since 5.8.0 + * @since 6.1.0 Added caching. + * + * @param string $file_path Path to file. Empty if no file. + * @return array Contents that adhere to the theme.json schema. + */ + protected static function read_json_file( $file_path ) { + if ( $file_path ) { + if ( array_key_exists( $file_path, static::$theme_json_file_cache ) ) { + return static::$theme_json_file_cache[ $file_path ]; + } + + $decoded_file = wp_json_file_decode( $file_path, array( 'associative' => true ) ); + if ( is_array( $decoded_file ) ) { + static::$theme_json_file_cache[ $file_path ] = $decoded_file; + return static::$theme_json_file_cache[ $file_path ]; + } + } + + return array(); + } + + /** + * Returns a data structure used in theme.json translation. + * + * @since 5.8.0 + * @deprecated 5.9.0 + * + * @return array An array of theme.json fields that are translatable and the keys that are translatable. + */ + public static function get_fields_to_translate() { + _deprecated_function( __METHOD__, '5.9.0' ); + return array(); + } + + /** + * Given a theme.json structure modifies it in place to update certain values + * by its translated strings according to the language set by the user. + * + * @since 5.8.0 + * + * @param array $theme_json The theme.json to translate. + * @param string $domain Optional. Text domain. Unique identifier for retrieving translated strings. + * Default 'default'. + * @return array Returns the modified $theme_json_structure. + */ + protected static function translate( $theme_json, $domain = 'default' ) { + if ( null === static::$i18n_schema ) { + $i18n_schema = wp_json_file_decode( __DIR__ . '/theme-i18n.json' ); + static::$i18n_schema = null === $i18n_schema ? array() : $i18n_schema; + } + + return translate_settings_using_i18n_schema( static::$i18n_schema, $theme_json, $domain ); + } + + /** + * Returns core's origin config. + * + * @since 5.8.0 + * + * @return WP_Theme_JSON Entity that holds core data. + */ + public static function get_core_data() { + if ( null !== static::$core && static::has_same_registered_blocks( 'core' ) ) { + return static::$core; + } + + $config = static::read_json_file( __DIR__ . '/theme.json' ); + $config = static::translate( $config ); + + /** + * Filters the default data provided by WordPress for global styles & settings. + * + * @since 6.1.0 + * + * @param WP_Theme_JSON_Data Class to access and update the underlying data. + */ + $theme_json = apply_filters( 'wp_theme_json_data_default', new WP_Theme_JSON_Data_Gutenberg( $config, 'default' ) ); + $config = $theme_json->get_data(); + static::$core = new WP_Theme_JSON_Gutenberg( $config, 'default' ); + + return static::$core; + } + + /** + * Checks whether the registered blocks were already processed for this origin. + * + * @since 6.1.0 + * + * @param string $origin Data source for which to cache the blocks. + * Valid values are 'core', 'blocks', 'theme', and 'user'. + * @return bool True on success, false otherwise. + */ + protected static function has_same_registered_blocks( $origin ) { + // Bail out if the origin is invalid. + if ( ! isset( static::$blocks_cache[ $origin ] ) ) { + return false; + } + + $registry = WP_Block_Type_Registry::get_instance(); + $blocks = $registry->get_all_registered(); + + // Is there metadata for all currently registered blocks? + $block_diff = array_diff_key( $blocks, static::$blocks_cache[ $origin ] ); + if ( empty( $block_diff ) ) { + return true; + } + + foreach ( $blocks as $block_name => $block_type ) { + static::$blocks_cache[ $origin ][ $block_name ] = true; + } + + return false; + } + + /** + * Returns the theme's data. + * + * Data from theme.json will be backfilled from existing + * theme supports, if any. Note that if the same data + * is present in theme.json and in theme supports, + * the theme.json takes precedence. + * + * @since 5.8.0 + * @since 5.9.0 Theme supports have been inlined and the `$theme_support_data` argument removed. + * @since 6.0.0 Added an `$options` parameter to allow the theme data to be returned without theme supports. + * + * @param array $deprecated Deprecated. Not used. + * @param array $options { + * Options arguments. + * + * @type bool $with_supports Whether to include theme supports in the data. Default true. + * } + * @return WP_Theme_JSON Entity that holds theme data. + */ + public static function get_theme_data( $deprecated = array(), $options = array() ) { + if ( ! empty( $deprecated ) ) { + _deprecated_argument( __METHOD__, '5.9.0' ); + } + + $options = wp_parse_args( $options, array( 'with_supports' => true ) ); + + if ( null === static::$theme || ! static::has_same_registered_blocks( 'theme' ) ) { + $theme_json_file = static::get_file_path_from_theme( 'theme.json' ); + $wp_theme = wp_get_theme(); + if ( '' !== $theme_json_file ) { + $theme_json_data = static::read_json_file( $theme_json_file ); + $theme_json_data = static::translate( $theme_json_data, $wp_theme->get( 'TextDomain' ) ); + } else { + $theme_json_data = array(); + } + // BEGIN OF EXPERIMENTAL CODE. Not to backport to core. + $theme_json_data = gutenberg_add_registered_webfonts_to_theme_json( $theme_json_data ); + // END OF EXPERIMENTAL CODE. + + /** + * Filters the data provided by the theme for global styles and settings. + * + * @since 6.1.0 + * + * @param WP_Theme_JSON_Data Class to access and update the underlying data. + */ + $theme_json = apply_filters( 'wp_theme_json_data_theme', new WP_Theme_JSON_Data_Gutenberg( $theme_json_data, 'theme' ) ); + $theme_json_data = $theme_json->get_data(); + static::$theme = new WP_Theme_JSON_Gutenberg( $theme_json_data ); + + if ( $wp_theme->parent() ) { + // Get parent theme.json. + $parent_theme_json_file = static::get_file_path_from_theme( 'theme.json', true ); + if ( '' !== $parent_theme_json_file ) { + $parent_theme_json_data = static::read_json_file( $parent_theme_json_file ); + $parent_theme_json_data = static::translate( $parent_theme_json_data, $wp_theme->parent()->get( 'TextDomain' ) ); + // BEGIN OF EXPERIMENTAL CODE. Not to backport to core. + $parent_theme_json_data = gutenberg_add_registered_webfonts_to_theme_json( $parent_theme_json_data ); + // END OF EXPERIMENTAL CODE. + $parent_theme = new WP_Theme_JSON_Gutenberg( $parent_theme_json_data ); + + /* + * Merge the child theme.json into the parent theme.json. + * The child theme takes precedence over the parent. + */ + $parent_theme->merge( static::$theme ); + static::$theme = $parent_theme; + } + } + } + + if ( ! $options['with_supports'] ) { + return static::$theme; + } + + /* + * We want the presets and settings declared in theme.json + * to override the ones declared via theme supports. + * So we take theme supports, transform it to theme.json shape + * and merge the static::$theme upon that. + */ + $theme_support_data = WP_Theme_JSON_Gutenberg::get_from_editor_settings( gutenberg_get_legacy_theme_supports_for_theme_json() ); + if ( ! wp_theme_has_theme_json() ) { + if ( ! isset( $theme_support_data['settings']['color'] ) ) { + $theme_support_data['settings']['color'] = array(); + } + + $default_palette = false; + if ( current_theme_supports( 'default-color-palette' ) ) { + $default_palette = true; + } + if ( ! isset( $theme_support_data['settings']['color']['palette'] ) ) { + // If the theme does not have any palette, we still want to show the core one. + $default_palette = true; + } + $theme_support_data['settings']['color']['defaultPalette'] = $default_palette; + + $default_gradients = false; + if ( current_theme_supports( 'default-gradient-presets' ) ) { + $default_gradients = true; + } + if ( ! isset( $theme_support_data['settings']['color']['gradients'] ) ) { + // If the theme does not have any gradients, we still want to show the core ones. + $default_gradients = true; + } + $theme_support_data['settings']['color']['defaultGradients'] = $default_gradients; + + // Classic themes without a theme.json don't support global duotone. + $theme_support_data['settings']['color']['defaultDuotone'] = false; + + // Allow themes to enable appearance tools via theme_support. + if ( current_theme_supports( 'appearance-tools' ) ) { + $theme_support_data['settings']['appearanceTools'] = true; + } + } + $with_theme_supports = new WP_Theme_JSON_Gutenberg( $theme_support_data ); + $with_theme_supports->merge( static::$theme ); + return $with_theme_supports; + } + + /** + * Gets the styles for blocks from the block.json file. + * + * @since 6.1.0 + * + * @return WP_Theme_JSON + */ + public static function get_block_data() { + $registry = WP_Block_Type_Registry::get_instance(); + $blocks = $registry->get_all_registered(); + + if ( null !== static::$blocks && static::has_same_registered_blocks( 'blocks' ) ) { + return static::$blocks; + } + + $config = array( 'version' => 2 ); + foreach ( $blocks as $block_name => $block_type ) { + if ( isset( $block_type->supports['__experimentalStyle'] ) ) { + $config['styles']['blocks'][ $block_name ] = static::remove_json_comments( $block_type->supports['__experimentalStyle'] ); + } + + if ( + isset( $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ) && + null === _wp_array_get( $config, array( 'styles', 'blocks', $block_name, 'spacing', 'blockGap' ), null ) + ) { + // Ensure an empty placeholder value exists for the block, if it provides a default blockGap value. + // The real blockGap value to be used will be determined when the styles are rendered for output. + $config['styles']['blocks'][ $block_name ]['spacing']['blockGap'] = null; + } + } + + /** + * Filters the data provided by the blocks for global styles & settings. + * + * @since 6.1.0 + * + * @param WP_Theme_JSON_Data Class to access and update the underlying data. + */ + $theme_json = apply_filters( 'wp_theme_json_data_blocks', new WP_Theme_JSON_Data_Gutenberg( $config, 'blocks' ) ); + $config = $theme_json->get_data(); + + static::$blocks = new WP_Theme_JSON_Gutenberg( $config, 'blocks' ); + return static::$blocks; + } + + /** + * When given an array, this will remove any keys with the name `//`. + * + * @param array $array The array to filter. + * @return array The filtered array. + */ + private static function remove_json_comments( $array ) { + unset( $array['//'] ); + foreach ( $array as $k => $v ) { + if ( is_array( $v ) ) { + $array[ $k ] = static::remove_json_comments( $v ); + } + } + + return $array; + } + + /** + * Returns the custom post type that contains the user's origin config + * for the active theme or a void array if none are found. + * + * This can also create and return a new draft custom post type. + * + * @since 5.9.0 + * + * @param WP_Theme $theme The theme object. If empty, it + * defaults to the active theme. + * @param bool $create_post Optional. Whether a new custom post + * type should be created if none are + * found. Default false. + * @param array $post_status_filter Optional. Filter custom post type by + * post status. Default `array( 'publish' )`, + * so it only fetches published posts. + * @return array Custom Post Type for the user's origin config. + */ + public static function get_user_data_from_wp_global_styles( $theme, $create_post = false, $post_status_filter = array( 'publish' ) ) { + if ( ! $theme instanceof WP_Theme ) { + $theme = wp_get_theme(); + } + + /* + * Bail early if the theme does not support a theme.json. + * + * Since wp_theme_has_theme_json only supports the active + * theme, the extra condition for whether $theme is the active theme is + * present here. + */ + if ( $theme->get_stylesheet() === get_stylesheet() && ! wp_theme_has_theme_json() ) { + return array(); + } + + $user_cpt = array(); + $post_type_filter = 'wp_global_styles'; + $stylesheet = $theme->get_stylesheet(); + $args = array( + 'posts_per_page' => 1, + 'orderby' => 'date', + 'order' => 'desc', + 'post_type' => $post_type_filter, + 'post_status' => $post_status_filter, + 'ignore_sticky_posts' => true, + 'no_found_rows' => true, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + 'tax_query' => array( + array( + 'taxonomy' => 'wp_theme', + 'field' => 'name', + 'terms' => $stylesheet, + ), + ), + ); + + $global_style_query = new WP_Query(); + $recent_posts = $global_style_query->query( $args ); + if ( count( $recent_posts ) === 1 ) { + $user_cpt = get_object_vars( $recent_posts[0] ); + } elseif ( $create_post ) { + $cpt_post_id = wp_insert_post( + array( + 'post_content' => '{"version": ' . WP_Theme_JSON_Gutenberg::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }', + 'post_status' => 'publish', + 'post_title' => 'Custom Styles', // Do not make string translatable, see https://core.trac.wordpress.org/ticket/54518. + 'post_type' => $post_type_filter, + 'post_name' => sprintf( 'wp-global-styles-%s', urlencode( $stylesheet ) ), + 'tax_input' => array( + 'wp_theme' => array( $stylesheet ), + ), + ), + true + ); + if ( ! is_wp_error( $cpt_post_id ) ) { + $user_cpt = get_object_vars( get_post( $cpt_post_id ) ); + } + } + + return $user_cpt; + } + + /** + * Returns the user's origin config. + * + * @since 5.9.0 + * + * @return WP_Theme_JSON Entity that holds styles for user data. + */ + public static function get_user_data() { + if ( null !== static::$user && static::has_same_registered_blocks( 'user' ) ) { + return static::$user; + } + + $config = array(); + $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme() ); + + if ( array_key_exists( 'post_content', $user_cpt ) ) { + $decoded_data = json_decode( $user_cpt['post_content'], true ); + + $json_decoding_error = json_last_error(); + if ( JSON_ERROR_NONE !== $json_decoding_error ) { + trigger_error( 'Error when decoding a theme.json schema for user data. ' . json_last_error_msg() ); + /** + * Filters the data provided by the user for global styles & settings. + * + * @since 6.1.0 + * + * @param WP_Theme_JSON_Data Class to access and update the underlying data. + */ + $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) ); + $config = $theme_json->get_data(); + return new WP_Theme_JSON_Gutenberg( $config, 'custom' ); + } + + // Very important to verify that the flag isGlobalStylesUserThemeJSON is true. + // If it's not true then the content was not escaped and is not safe. + if ( + is_array( $decoded_data ) && + isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) && + $decoded_data['isGlobalStylesUserThemeJSON'] + ) { + unset( $decoded_data['isGlobalStylesUserThemeJSON'] ); + $config = $decoded_data; + } + } + + /** This filter is documented in wp-includes/class-wp-theme-json-resolver.php */ + $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) ); + $config = $theme_json->get_data(); + static::$user = new WP_Theme_JSON_Gutenberg( $config, 'custom' ); + + return static::$user; + } + + /** + * Returns the data merged from multiple origins. + * + * There are four sources of data (origins) for a site: + * + * - default => WordPress + * - blocks => each one of the blocks provides data for itself + * - theme => the active theme + * - custom => data provided by the user + * + * The custom's has higher priority than the theme's, the theme's higher than blocks', + * and block's higher than default's. + * + * Unlike the getters + * {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_core_data/ get_core_data}, + * {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_theme_data/ get_theme_data}, + * and {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_user_data/ get_user_data}, + * this method returns data after it has been merged with the previous origins. + * This means that if the same piece of data is declared in different origins + * (default, blocks, theme, custom), the last origin overrides the previous. + * + * For example, if the user has set a background color + * for the paragraph block, and the theme has done it as well, + * the user preference wins. + * + * @since 5.8.0 + * @since 5.9.0 Added user data, removed the `$settings` parameter, + * added the `$origin` parameter. + * @since 6.1.0 Added block data and generation of spacingSizes array. + * + * @param string $origin Optional. To what level should we merge data:'default', 'blocks', 'theme' or 'custom'. + * 'custom' is used as default value as well as fallback value if the origin is unknown. + * + * @return WP_Theme_JSON + */ + public static function get_merged_data( $origin = 'custom' ) { + if ( is_array( $origin ) ) { + _deprecated_argument( __FUNCTION__, '5.9.0' ); + } + + $result = static::get_core_data(); + if ( 'default' === $origin ) { + $result->set_spacing_sizes(); + return $result; + } + + $result->merge( static::get_block_data() ); + if ( 'blocks' === $origin ) { + return $result; + } + + $result->merge( static::get_theme_data() ); + if ( 'theme' === $origin ) { + $result->set_spacing_sizes(); + return $result; + } + + $result->merge( static::get_user_data() ); + $result->set_spacing_sizes(); + return $result; + } + + /** + * Returns the ID of the custom post type + * that stores user data. + * + * @since 5.9.0 + * + * @return integer|null + */ + public static function get_user_global_styles_post_id() { + if ( null !== static::$user_custom_post_type_id ) { + return static::$user_custom_post_type_id; + } + + $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme(), true ); + + if ( array_key_exists( 'ID', $user_cpt ) ) { + static::$user_custom_post_type_id = $user_cpt['ID']; + } + + return static::$user_custom_post_type_id; + } + + /** + * Determines whether the active theme has a theme.json file. + * + * @since 5.8.0 + * @since 5.9.0 Added a check in the parent theme. + * @deprecated 6.2.0 Use wp_theme_has_theme_json() instead. + * + * @return bool + */ + public static function theme_has_support() { + _deprecated_function( __METHOD__, '6.2.0', 'wp_theme_has_theme_json()' ); + + return wp_theme_has_theme_json(); + } + + /** + * Builds the path to the given file and checks that it is readable. + * + * If it isn't, returns an empty string, otherwise returns the whole file path. + * + * @since 5.8.0 + * @since 5.9.0 Adapted to work with child themes, added the `$template` argument. + * + * @param string $file_name Name of the file. + * @param bool $template Optional. Use template theme directory. Default false. + * @return string The whole file path or empty if the file doesn't exist. + */ + protected static function get_file_path_from_theme( $file_name, $template = false ) { + $path = $template ? get_template_directory() : get_stylesheet_directory(); + $candidate = $path . '/' . $file_name; + + return is_readable( $candidate ) ? $candidate : ''; + } + + /** + * Cleans the cached data so it can be recalculated. + * + * @since 5.8.0 + * @since 5.9.0 Added the `$user`, `$user_custom_post_type_id`, + * and `$i18n_schema` variables to reset. + * @since 6.1.0 Added the `$blocks` and `$blocks_cache` variables + * to reset. + */ + public static function clean_cached_data() { + static::$core = null; + static::$blocks = null; + static::$blocks_cache = array( + 'core' => array(), + 'blocks' => array(), + 'theme' => array(), + 'user' => array(), + ); + static::$theme = null; + static::$user = null; + static::$user_custom_post_type_id = null; + static::$i18n_schema = null; + } + + /** + * Returns the style variations defined by the theme. + * + * @since 6.0.0 + * + * @return array + */ + public static function get_style_variations() { + $variations = array(); + $base_directory = get_stylesheet_directory() . '/styles'; + if ( is_dir( $base_directory ) ) { + $nested_files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $base_directory ) ); + $nested_html_files = iterator_to_array( new RegexIterator( $nested_files, '/^.+\.json$/i', RecursiveRegexIterator::GET_MATCH ) ); + ksort( $nested_html_files ); + foreach ( $nested_html_files as $path => $file ) { + $decoded_file = wp_json_file_decode( $path, array( 'associative' => true ) ); + if ( is_array( $decoded_file ) ) { + $translated = static::translate( $decoded_file, wp_get_theme()->get( 'TextDomain' ) ); + $variation = ( new WP_Theme_JSON_Gutenberg( $translated ) )->get_raw_data(); + if ( empty( $variation['title'] ) ) { + $variation['title'] = basename( $path, '.json' ); + } + $variations[] = $variation; + } + } + } + return $variations; + } + +} diff --git a/lib/client-assets.php b/lib/client-assets.php index b7a11724a5e096..e23e1c4b89bd76 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -274,7 +274,7 @@ function gutenberg_register_packages_styles( $styles ) { $styles, 'wp-editor', gutenberg_url( 'build/editor/style.css' ), - array( 'wp-components', 'wp-block-editor', 'wp-nux', 'wp-reusable-blocks' ), + array( 'wp-components', 'wp-block-editor', 'wp-reusable-blocks' ), $version ); $styles->add_data( 'wp-editor', 'rtl', 'replace' ); @@ -283,7 +283,7 @@ function gutenberg_register_packages_styles( $styles ) { $styles, 'wp-edit-post', gutenberg_url( 'build/edit-post/style.css' ), - array( 'wp-components', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-block-library', 'wp-nux' ), + array( 'wp-components', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-block-library' ), $version ); $styles->add_data( 'wp-edit-post', 'rtl', 'replace' ); @@ -367,15 +367,6 @@ function gutenberg_register_packages_styles( $styles ) { ); $styles->add_data( 'wp-edit-blocks', 'rtl', 'replace' ); - gutenberg_override_style( - $styles, - 'wp-nux', - gutenberg_url( 'build/nux/style.css' ), - array( 'wp-components' ), - $version - ); - $styles->add_data( 'wp-nux', 'rtl', 'replace' ); - gutenberg_override_style( $styles, 'wp-block-library-theme', @@ -558,13 +549,15 @@ function gutenberg_register_vendor_scripts( $scripts ) { 'react', gutenberg_url( 'build/vendors/react' . $extension ), // See https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/docs/TROUBLESHOOTING.md#externalising-react. - SCRIPT_DEBUG ? array( 'wp-react-refresh-entry', 'wp-polyfill' ) : array( 'wp-polyfill' ) + SCRIPT_DEBUG ? array( 'wp-react-refresh-entry', 'wp-polyfill' ) : array( 'wp-polyfill' ), + '18' ); gutenberg_override_script( $scripts, 'react-dom', gutenberg_url( 'build/vendors/react-dom' . $extension ), - array( 'react' ) + array( 'react' ), + '18' ); } add_action( 'wp_default_scripts', 'gutenberg_register_vendor_scripts' ); diff --git a/lib/compat/wordpress-6.1/class-gutenberg-rest-templates-controller.php b/lib/compat/wordpress-6.1/class-gutenberg-rest-templates-controller.php index e911b0f1e4d203..f0416e0a50e96f 100644 --- a/lib/compat/wordpress-6.1/class-gutenberg-rest-templates-controller.php +++ b/lib/compat/wordpress-6.1/class-gutenberg-rest-templates-controller.php @@ -55,7 +55,7 @@ public function register_routes() { * @return WP_REST_Response|WP_Error */ public function get_template_fallback( $request ) { - $hierarchy = get_template_hierarchy( $request['slug'], $request['is_custom'], $request['template_prefix'] ); + $hierarchy = gutenberg_get_template_hierarchy( $request['slug'], $request['is_custom'], $request['template_prefix'] ); $fallback_template = resolve_block_template( $request['slug'], $hierarchy, '' ); $response = $this->prepare_item_for_response( $fallback_template, $request ); return rest_ensure_response( $response ); diff --git a/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php b/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php deleted file mode 100644 index 1d4a83e3406fbe..00000000000000 --- a/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php +++ /dev/null @@ -1,191 +0,0 @@ -get_data(); - static::$core = new WP_Theme_JSON_Gutenberg( $config, 'default' ); - - return static::$core; - } - - /** - * Returns the user's origin config. - * - * @return WP_Theme_JSON_Gutenberg Entity that holds styles for user data. - */ - public static function get_user_data() { - if ( null !== static::$user ) { - return static::$user; - } - - $config = array(); - $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme() ); - - if ( array_key_exists( 'post_content', $user_cpt ) ) { - $decoded_data = json_decode( $user_cpt['post_content'], true ); - - $json_decoding_error = json_last_error(); - if ( JSON_ERROR_NONE !== $json_decoding_error ) { - trigger_error( 'Error when decoding a theme.json schema for user data. ' . json_last_error_msg() ); - /** - * Filters the data provided by the user for global styles & settings. - * - * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data. - */ - $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) ); - $config = $theme_json->get_data(); - return new WP_Theme_JSON_Gutenberg( $config, 'custom' ); - } - - // Very important to verify if the flag isGlobalStylesUserThemeJSON is true. - // If is not true the content was not escaped and is not safe. - if ( - is_array( $decoded_data ) && - isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) && - $decoded_data['isGlobalStylesUserThemeJSON'] - ) { - unset( $decoded_data['isGlobalStylesUserThemeJSON'] ); - $config = $decoded_data; - } - } - - /** - * Filters the data provided by the user for global styles & settings. - * - * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data. - */ - $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) ); - $config = $theme_json->get_data(); - static::$user = new WP_Theme_JSON_Gutenberg( $config, 'custom' ); - - return static::$user; - } - - /** - * Returns the custom post type that contains the user's origin config - * for the active theme or a void array if none are found. - * - * This can also create and return a new draft custom post type. - * - * @since 5.9.0 - * - * @param WP_Theme $theme The theme object. If empty, it - * defaults to the active theme. - * @param bool $create_post Optional. Whether a new custom post - * type should be created if none are - * found. Default false. - * @param array $post_status_filter Optional. Filter custom post type by - * post status. Default `array( 'publish' )`, - * so it only fetches published posts. - * @return array Custom Post Type for the user's origin config. - */ - public static function get_user_data_from_wp_global_styles( $theme, $create_post = false, $post_status_filter = array( 'publish' ) ) { - if ( ! $theme instanceof WP_Theme ) { - $theme = wp_get_theme(); - } - $user_cpt = array(); - $post_type_filter = 'wp_global_styles'; - $stylesheet = $theme->get_stylesheet(); - $args = array( - 'posts_per_page' => 1, - 'orderby' => 'date', - 'order' => 'desc', - 'post_type' => $post_type_filter, - 'post_status' => $post_status_filter, - 'ignore_sticky_posts' => true, - 'no_found_rows' => true, - 'tax_query' => array( - array( - 'taxonomy' => 'wp_theme', - 'field' => 'name', - 'terms' => $stylesheet, - ), - ), - ); - - $global_style_query = new WP_Query(); - $recent_posts = $global_style_query->query( $args ); - if ( count( $recent_posts ) === 1 ) { - $user_cpt = get_post( $recent_posts[0], ARRAY_A ); - } elseif ( $create_post ) { - $cpt_post_id = wp_insert_post( - array( - 'post_content' => '{"version": ' . WP_Theme_JSON_Gutenberg::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }', - 'post_status' => 'publish', - 'post_title' => 'Custom Styles', // Do not make string translatable, see https://core.trac.wordpress.org/ticket/54518. - 'post_type' => $post_type_filter, - 'post_name' => sprintf( 'wp-global-styles-%s', urlencode( $stylesheet ) ), - 'tax_input' => array( - 'wp_theme' => array( $stylesheet ), - ), - ), - true - ); - if ( ! is_wp_error( $cpt_post_id ) ) { - $user_cpt = get_post( $cpt_post_id, ARRAY_A ); - } - } - - return $user_cpt; - } -} diff --git a/lib/compat/wordpress-6.2/block-patterns.php b/lib/compat/wordpress-6.2/block-patterns.php index 1e529faf963210..13d9af96220494 100644 --- a/lib/compat/wordpress-6.2/block-patterns.php +++ b/lib/compat/wordpress-6.2/block-patterns.php @@ -225,6 +225,7 @@ function gutenberg_register_theme_block_patterns() { 'blockTypes' => 'Block Types', 'postTypes' => 'Post Types', 'inserter' => 'Inserter', + 'templateTypes' => 'Template Types', ); /* @@ -294,7 +295,7 @@ function gutenberg_register_theme_block_patterns() { } // For properties of type array, parse data as comma-separated. - foreach ( array( 'categories', 'keywords', 'blockTypes', 'postTypes' ) as $property ) { + foreach ( array( 'categories', 'keywords', 'blockTypes', 'postTypes', 'templateTypes' ) as $property ) { if ( ! empty( $pattern_data[ $property ] ) ) { $pattern_data[ $property ] = array_filter( preg_split( diff --git a/lib/compat/wordpress-6.2/block-template-utils.php b/lib/compat/wordpress-6.2/block-template-utils.php new file mode 100644 index 00000000000000..832b9714dd13d3 --- /dev/null +++ b/lib/compat/wordpress-6.2/block-template-utils.php @@ -0,0 +1,112 @@ + The template hierarchy. + */ +function gutenberg_get_template_hierarchy( $slug, $is_custom = false, $template_prefix = '' ) { + if ( 'index' === $slug ) { + return array( 'index' ); + } + if ( $is_custom ) { + return array( 'page', 'singular', 'index' ); + } + if ( 'front-page' === $slug ) { + return array( 'front-page', 'home', 'index' ); + } + + $template_hierarchy = array( $slug ); + // Most default templates don't have `$template_prefix` assigned. + if ( ! empty( $template_prefix ) ) { + list($type) = explode( '-', $template_prefix ); + // We need these checks because we always add the `$slug` above. + if ( ! in_array( $template_prefix, array( $slug, $type ), true ) ) { + $template_hierarchy[] = $template_prefix; + } + if ( $slug !== $type ) { + $template_hierarchy[] = $type; + } + } else { + $matches = array(); + if ( preg_match( '/^(author|category|archive|tag|page)-(.+)$/', $slug, $matches ) ) { + $template_hierarchy[] = $matches[1]; + } elseif ( preg_match( '/^(single|taxonomy)-(.+)$/', $slug, $matches ) ) { + $type = $matches[1]; + $slug_remaining = $matches[2]; + if ( 'single' === $type ) { + $post_types = get_post_types(); + foreach ( $post_types as $post_type ) { + if ( str_starts_with( $slug_remaining, $post_type ) ) { + // If $slug_remaining is equal to $post_type we have the single-$post_type template. + if ( $slug_remaining === $post_type ) { + $template_hierarchy[] = 'single'; + break; + } + // If $slug_remaining is single-$post_type-$slug template. + if ( str_starts_with( $slug_remaining, $post_type . '-' ) && strlen( $slug_remaining ) > strlen( $post_type ) + 1 ) { + $template_hierarchy[] = "single-$post_type"; + $template_hierarchy[] = 'single'; + break; + } + } + } + } elseif ( 'taxonomy' === $type ) { + $taxonomies = get_taxonomies(); + foreach ( $taxonomies as $taxonomy ) { + if ( str_starts_with( $slug_remaining, $taxonomy ) ) { + // If $slug_remaining is equal to $taxonomy we have the taxonomy-$taxonomy template. + if ( $slug_remaining === $taxonomy ) { + $template_hierarchy[] = 'taxonomy'; + break; + } + // If $slug_remaining is taxonomy-$taxonomy-$term template. + if ( str_starts_with( $slug_remaining, $taxonomy . '-' ) && strlen( $slug_remaining ) > strlen( $taxonomy ) + 1 ) { + $template_hierarchy[] = "taxonomy-$taxonomy"; + $template_hierarchy[] = 'taxonomy'; + break; + } + } + } + } + } + } + // Handle `archive` template. + if ( + str_starts_with( $slug, 'author' ) || + str_starts_with( $slug, 'taxonomy' ) || + str_starts_with( $slug, 'category' ) || + str_starts_with( $slug, 'tag' ) || + 'date' === $slug + ) { + $template_hierarchy[] = 'archive'; + } + // Handle `single` template. + if ( 'attachment' === $slug ) { + $template_hierarchy[] = 'single'; + } + // Handle `singular` template. + if ( + str_starts_with( $slug, 'single' ) || + str_starts_with( $slug, 'page' ) || + 'attachment' === $slug + ) { + $template_hierarchy[] = 'singular'; + } + $template_hierarchy[] = 'index'; + return $template_hierarchy; +} diff --git a/lib/compat/wordpress-6.2/class-gutenberg-rest-block-patterns-controller-6-2.php b/lib/compat/wordpress-6.2/class-gutenberg-rest-block-patterns-controller-6-2.php index d34160edb2af0b..604818fcd30a70 100644 --- a/lib/compat/wordpress-6.2/class-gutenberg-rest-block-patterns-controller-6-2.php +++ b/lib/compat/wordpress-6.2/class-gutenberg-rest-block-patterns-controller-6-2.php @@ -34,6 +34,128 @@ class Gutenberg_REST_Block_Patterns_Controller_6_2 extends Gutenberg_REST_Block_ 'query' => 'posts', ); + /** + * Prepare a raw block pattern before it gets output in a REST API response. + * + * @since 6.0.0 + * + * @param array $item Raw pattern as registered, before any changes. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { + $fields = $this->get_fields_for_response( $request ); + $keys = array( + 'name' => 'name', + 'title' => 'title', + 'description' => 'description', + 'viewportWidth' => 'viewport_width', + 'blockTypes' => 'block_types', + 'postTypes' => 'post_types', + 'categories' => 'categories', + 'keywords' => 'keywords', + 'content' => 'content', + 'inserter' => 'inserter', + 'templateTypes' => 'template_types', + ); + $data = array(); + foreach ( $keys as $item_key => $rest_key ) { + if ( isset( $item[ $item_key ] ) && rest_is_field_included( $rest_key, $fields ) ) { + $data[ $rest_key ] = $item[ $item_key ]; + } + } + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + return rest_ensure_response( $data ); + } + + /** + * Retrieves the block pattern schema, conforming to JSON Schema. + * + * @since 6.0.0 + * @since 6.1.0 Added `post_types` property. + * + * @return array Item schema data. + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'block-pattern', + 'type' => 'object', + 'properties' => array( + 'name' => array( + 'description' => __( 'The pattern name.', 'gutenberg' ), + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'title' => array( + 'description' => __( 'The pattern title, in human readable format.', 'gutenberg' ), + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'description' => array( + 'description' => __( 'The pattern detailed description.', 'gutenberg' ), + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'viewport_width' => array( + 'description' => __( 'The pattern viewport width for inserter preview.', 'gutenberg' ), + 'type' => 'number', + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'block_types' => array( + 'description' => __( 'Block types that the pattern is intended to be used with.', 'gutenberg' ), + 'type' => 'array', + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'post_types' => array( + 'description' => __( 'An array of post types that the pattern is restricted to be used with.', 'gutenberg' ), + 'type' => 'array', + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'categories' => array( + 'description' => __( 'The pattern category slugs.', 'gutenberg' ), + 'type' => 'array', + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'keywords' => array( + 'description' => __( 'The pattern keywords.', 'gutenberg' ), + 'type' => 'array', + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'template_types' => array( + 'description' => __( 'An array of template types where the pattern fits.', 'gutenberg' ), + 'type' => 'array', + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'content' => array( + 'description' => __( 'The pattern content.', 'gutenberg' ), + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'inserter' => array( + 'description' => __( 'Determines whether the pattern is visible in inserter.', 'gutenberg' ), + 'type' => 'boolean', + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + /** * Registers the routes for the objects of the controller. * diff --git a/lib/compat/wordpress-6.2/class-gutenberg-rest-global-styles-controller-6-2.php b/lib/compat/wordpress-6.2/class-gutenberg-rest-global-styles-controller-6-2.php index 9f762dd961d058..643374aba490cc 100644 --- a/lib/compat/wordpress-6.2/class-gutenberg-rest-global-styles-controller-6-2.php +++ b/lib/compat/wordpress-6.2/class-gutenberg-rest-global-styles-controller-6-2.php @@ -151,10 +151,12 @@ protected function prepare_item_for_database( $request ) { if ( isset( $request['styles'] ) || isset( $request['settings'] ) ) { $config = array(); if ( isset( $request['styles'] ) ) { - $config['styles'] = $request['styles']; - $validate_custom_css = $this->validate_custom_css( $request['styles']['css'] ); - if ( is_wp_error( $validate_custom_css ) ) { - return $validate_custom_css; + $config['styles'] = $request['styles']; + if ( isset( $request['styles']['css'] ) ) { + $validate_custom_css = $this->validate_custom_css( $request['styles']['css'] ); + if ( is_wp_error( $validate_custom_css ) ) { + return $validate_custom_css; + } } } elseif ( isset( $existing_config['styles'] ) ) { $config['styles'] = $existing_config['styles']; @@ -199,4 +201,55 @@ private function validate_custom_css( $css ) { } return true; } + + /** + * Returns the given theme global styles config. + * Duplicated from core. + * The only change is that we call WP_Theme_JSON_Resolver_Gutenberg::get_merged_data( 'theme' ) instead of WP_Theme_JSON_Resolver::get_merged_data( 'theme' ). + * + * @since 6.2.0 + * + * @param WP_REST_Request $request The request instance. + * @return WP_REST_Response|WP_Error + */ + public function get_theme_item( $request ) { + if ( get_stylesheet() !== $request['stylesheet'] ) { + // This endpoint only supports the active theme for now. + return new WP_Error( + 'rest_theme_not_found', + __( 'Theme not found.', 'gutenberg' ), + array( 'status' => 404 ) + ); + } + + $theme = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data( 'theme' ); + $data = array(); + $fields = $this->get_fields_for_response( $request ); + + if ( rest_is_field_included( 'settings', $fields ) ) { + $data['settings'] = $theme->get_settings(); + } + + if ( rest_is_field_included( 'styles', $fields ) ) { + $raw_data = $theme->get_raw_data(); + $data['styles'] = isset( $raw_data['styles'] ) ? $raw_data['styles'] : array(); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '%s/%s/themes/%s', $this->namespace, $this->rest_base, $request['stylesheet'] ) ), + ), + ); + $response->add_links( $links ); + } + + return $response; + } } diff --git a/lib/compat/wordpress-6.2/class-wp-theme-json-6-2.php b/lib/compat/wordpress-6.2/class-wp-theme-json-6-2.php deleted file mode 100644 index fe01eb273066e2..00000000000000 --- a/lib/compat/wordpress-6.2/class-wp-theme-json-6-2.php +++ /dev/null @@ -1,391 +0,0 @@ - array( 'color', 'gradient' ), - 'background-color' => array( 'color', 'background' ), - 'border-radius' => array( 'border', 'radius' ), - 'border-top-left-radius' => array( 'border', 'radius', 'topLeft' ), - 'border-top-right-radius' => array( 'border', 'radius', 'topRight' ), - 'border-bottom-left-radius' => array( 'border', 'radius', 'bottomLeft' ), - 'border-bottom-right-radius' => array( 'border', 'radius', 'bottomRight' ), - 'border-color' => array( 'border', 'color' ), - 'border-width' => array( 'border', 'width' ), - 'border-style' => array( 'border', 'style' ), - 'border-top-color' => array( 'border', 'top', 'color' ), - 'border-top-width' => array( 'border', 'top', 'width' ), - 'border-top-style' => array( 'border', 'top', 'style' ), - 'border-right-color' => array( 'border', 'right', 'color' ), - 'border-right-width' => array( 'border', 'right', 'width' ), - 'border-right-style' => array( 'border', 'right', 'style' ), - 'border-bottom-color' => array( 'border', 'bottom', 'color' ), - 'border-bottom-width' => array( 'border', 'bottom', 'width' ), - 'border-bottom-style' => array( 'border', 'bottom', 'style' ), - 'border-left-color' => array( 'border', 'left', 'color' ), - 'border-left-width' => array( 'border', 'left', 'width' ), - 'border-left-style' => array( 'border', 'left', 'style' ), - 'color' => array( 'color', 'text' ), - 'font-family' => array( 'typography', 'fontFamily' ), - 'font-size' => array( 'typography', 'fontSize' ), - 'font-style' => array( 'typography', 'fontStyle' ), - 'font-weight' => array( 'typography', 'fontWeight' ), - 'letter-spacing' => array( 'typography', 'letterSpacing' ), - 'line-height' => array( 'typography', 'lineHeight' ), - 'margin' => array( 'spacing', 'margin' ), - 'margin-top' => array( 'spacing', 'margin', 'top' ), - 'margin-right' => array( 'spacing', 'margin', 'right' ), - 'margin-bottom' => array( 'spacing', 'margin', 'bottom' ), - 'margin-left' => array( 'spacing', 'margin', 'left' ), - 'min-height' => array( 'dimensions', 'minHeight' ), - 'padding' => array( 'spacing', 'padding' ), - 'padding-top' => array( 'spacing', 'padding', 'top' ), - 'padding-right' => array( 'spacing', 'padding', 'right' ), - 'padding-bottom' => array( 'spacing', 'padding', 'bottom' ), - 'padding-left' => array( 'spacing', 'padding', 'left' ), - '--wp--style--root--padding' => array( 'spacing', 'padding' ), - '--wp--style--root--padding-top' => array( 'spacing', 'padding', 'top' ), - '--wp--style--root--padding-right' => array( 'spacing', 'padding', 'right' ), - '--wp--style--root--padding-bottom' => array( 'spacing', 'padding', 'bottom' ), - '--wp--style--root--padding-left' => array( 'spacing', 'padding', 'left' ), - 'text-decoration' => array( 'typography', 'textDecoration' ), - 'text-transform' => array( 'typography', 'textTransform' ), - 'filter' => array( 'filter', 'duotone' ), - 'box-shadow' => array( 'shadow' ), - ); - - /** - * Indirect metadata for style properties that are not directly output. - * - * Each element is a direct mapping from a CSS property name to the - * path to the value in theme.json & block attributes. - * - * Indirect properties are not output directly by `compute_style_properties`, - * but are used elsewhere in the processing of global styles. The indirect - * property is used to validate whether or not a style value is allowed. - * - * @since 6.2.0 - * @var array - */ - const INDIRECT_PROPERTIES_METADATA = array( - 'gap' => array( 'spacing', 'blockGap' ), - 'column-gap' => array( 'spacing', 'blockGap', 'left' ), - 'row-gap' => array( 'spacing', 'blockGap', 'top' ), - ); - - /** - * The valid properties under the settings key. - * - * @since 5.8.0 As `ALLOWED_SETTINGS`. - * @since 5.9.0 Renamed from `ALLOWED_SETTINGS` to `VALID_SETTINGS`, - * added new properties for `border`, `color`, `spacing`, - * and `typography`, and renamed others according to the new schema. - * @since 6.0.0 Added `color.defaultDuotone`. - * @since 6.1.0 Added `layout.definitions` and `useRootPaddingAwareAlignments`. - * @since 6.2.0 Added `dimensions.minHeight`. - * @var array - */ - const VALID_SETTINGS = array( - 'appearanceTools' => null, - 'useRootPaddingAwareAlignments' => null, - 'border' => array( - 'color' => null, - 'radius' => null, - 'style' => null, - 'width' => null, - ), - 'color' => array( - 'background' => null, - 'custom' => null, - 'customDuotone' => null, - 'customGradient' => null, - 'defaultDuotone' => null, - 'defaultGradients' => null, - 'defaultPalette' => null, - 'duotone' => null, - 'gradients' => null, - 'link' => null, - 'palette' => null, - 'text' => null, - ), - 'custom' => null, - 'dimensions' => array( - 'minHeight' => null, - ), - 'layout' => array( - 'contentSize' => null, - 'definitions' => null, - 'wideSize' => null, - ), - 'spacing' => array( - 'customSpacingSize' => null, - 'spacingSizes' => null, - 'spacingScale' => null, - 'blockGap' => null, - 'margin' => null, - 'padding' => null, - 'units' => null, - ), - 'typography' => array( - 'fluid' => null, - 'customFontSize' => null, - 'dropCap' => null, - 'fontFamilies' => null, - 'fontSizes' => null, - 'fontStyle' => null, - 'fontWeight' => null, - 'letterSpacing' => null, - 'lineHeight' => null, - 'textDecoration' => null, - 'textTransform' => null, - ), - ); - - /** - * The valid properties under the styles key. - * - * @since 5.8.0 As `ALLOWED_STYLES`. - * @since 5.9.0 Renamed from `ALLOWED_STYLES` to `VALID_STYLES`, - * added new properties for `border`, `filter`, `spacing`, - * and `typography`. - * @since 6.1.0 Added new side properties for `border`, - * added new property `shadow`, - * updated `blockGap` to be allowed at any level. - * @since 6.2.0 Added new property `css`. - * @var array - */ - const VALID_STYLES = array( - 'border' => array( - 'color' => null, - 'radius' => null, - 'style' => null, - 'width' => null, - 'top' => null, - 'right' => null, - 'bottom' => null, - 'left' => null, - ), - 'color' => array( - 'background' => null, - 'gradient' => null, - 'text' => null, - ), - 'dimensions' => array( - 'minHeight' => null, - ), - 'filter' => array( - 'duotone' => null, - ), - 'shadow' => null, - 'spacing' => array( - 'margin' => null, - 'padding' => null, - 'blockGap' => null, - ), - 'typography' => array( - 'fontFamily' => null, - 'fontSize' => null, - 'fontStyle' => null, - 'fontWeight' => null, - 'letterSpacing' => null, - 'lineHeight' => null, - 'textDecoration' => null, - 'textTransform' => null, - ), - 'css' => null, - ); - - /** - * Processes a style node and returns the same node - * without the insecure styles. - * - * @since 5.9.0 - * @since 6.2.0 Allow indirect properties used outside of `compute_style_properties`. - * - * @param array $input Node to process. - * @return array - */ - protected static function remove_insecure_styles( $input ) { - $output = array(); - $declarations = static::compute_style_properties( $input ); - - foreach ( $declarations as $declaration ) { - if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) { - $path = static::PROPERTIES_METADATA[ $declaration['name'] ]; - - // Check the value isn't an array before adding so as to not - // double up shorthand and longhand styles. - $value = _wp_array_get( $input, $path, array() ); - if ( ! is_array( $value ) ) { - _wp_array_set( $output, $path, $value ); - } - } - } - - // Ensure indirect properties not handled by `compute_style_properties` are allowed. - foreach ( static::INDIRECT_PROPERTIES_METADATA as $property => $path ) { - $value = _wp_array_get( $input, $path, array() ); - if ( - isset( $value ) && - ! is_array( $value ) && - static::is_safe_css_declaration( $property, $value ) - ) { - _wp_array_set( $output, $path, $value ); - } - } - - return $output; - } - - /** - * Returns the stylesheet that results of processing - * the theme.json structure this object represents. - * - * @param array $types Types of styles to load. Will load all by default. It accepts: - * 'variables': only the CSS Custom Properties for presets & custom ones. - * 'styles': only the styles section in theme.json. - * 'presets': only the classes for the presets. - * 'custom-css': only the css from global styles.css. - * @param array $origins A list of origins to include. By default it includes VALID_ORIGINS. - * @param array $options An array of options for now used for internal purposes only (may change without notice). - * The options currently supported are 'scope' that makes sure all style are scoped to a given selector, - * and root_selector which overwrites and forces a given selector to be used on the root node. - * @return string Stylesheet. - */ - public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null, $options = array() ) { - if ( null === $origins ) { - $origins = static::VALID_ORIGINS; - } - - if ( is_string( $types ) ) { - // Dispatch error and map old arguments to new ones. - _deprecated_argument( __FUNCTION__, '5.9' ); - if ( 'block_styles' === $types ) { - $types = array( 'styles', 'presets' ); - } elseif ( 'css_variables' === $types ) { - $types = array( 'variables' ); - } else { - $types = array( 'variables', 'styles', 'presets' ); - } - } - - $blocks_metadata = static::get_blocks_metadata(); - $style_nodes = static::get_style_nodes( $this->theme_json, $blocks_metadata ); - $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata ); - - $root_style_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $style_nodes, 'selector' ), true ); - $root_settings_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $setting_nodes, 'selector' ), true ); - - if ( ! empty( $options['scope'] ) ) { - foreach ( $setting_nodes as &$node ) { - $node['selector'] = static::scope_selector( $options['scope'], $node['selector'] ); - } - foreach ( $style_nodes as &$node ) { - $node['selector'] = static::scope_selector( $options['scope'], $node['selector'] ); - } - } - - if ( ! empty( $options['root_selector'] ) ) { - if ( false !== $root_settings_key ) { - $setting_nodes[ $root_settings_key ]['selector'] = $options['root_selector']; - } - if ( false !== $root_style_key ) { - $setting_nodes[ $root_style_key ]['selector'] = $options['root_selector']; - } - } - - $stylesheet = ''; - - if ( in_array( 'variables', $types, true ) ) { - $stylesheet .= $this->get_css_variables( $setting_nodes, $origins ); - } - - if ( in_array( 'styles', $types, true ) ) { - if ( false !== $root_style_key ) { - $stylesheet .= $this->get_root_layout_rules( $style_nodes[ $root_style_key ]['selector'], $style_nodes[ $root_style_key ] ); - } - $stylesheet .= $this->get_block_classes( $style_nodes ); - } elseif ( in_array( 'base-layout-styles', $types, true ) ) { - $root_selector = static::ROOT_BLOCK_SELECTOR; - $columns_selector = '.wp-block-columns'; - if ( ! empty( $options['scope'] ) ) { - $root_selector = static::scope_selector( $options['scope'], $root_selector ); - $columns_selector = static::scope_selector( $options['scope'], $columns_selector ); - } - if ( ! empty( $options['root_selector'] ) ) { - $root_selector = $options['root_selector']; - } - // Base layout styles are provided as part of `styles`, so only output separately if explicitly requested. - // For backwards compatibility, the Columns block is explicitly included, to support a different default gap value. - $base_styles_nodes = array( - array( - 'path' => array( 'styles' ), - 'selector' => $root_selector, - ), - array( - 'path' => array( 'styles', 'blocks', 'core/columns' ), - 'selector' => $columns_selector, - 'name' => 'core/columns', - ), - ); - - foreach ( $base_styles_nodes as $base_style_node ) { - $stylesheet .= $this->get_layout_styles( $base_style_node ); - } - } - - if ( in_array( 'presets', $types, true ) ) { - $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins ); - } - - // Load the custom CSS last so it has the highest specificity. - if ( in_array( 'custom-css', $types, true ) ) { - $stylesheet .= _wp_array_get( $this->theme_json, array( 'styles', 'css' ) ); - } - - return $stylesheet; - } -} diff --git a/lib/compat/wordpress-6.2/class-wp-theme-json-resolver-6-2.php b/lib/compat/wordpress-6.2/class-wp-theme-json-resolver-6-2.php deleted file mode 100644 index 110c8bac7b147c..00000000000000 --- a/lib/compat/wordpress-6.2/class-wp-theme-json-resolver-6-2.php +++ /dev/null @@ -1,230 +0,0 @@ -get_stylesheet() === get_stylesheet() && ! wp_theme_has_theme_json() ) { - return array(); - } - - $user_cpt = array(); - $post_type_filter = 'wp_global_styles'; - $stylesheet = $theme->get_stylesheet(); - $args = array( - 'posts_per_page' => 1, - 'orderby' => 'date', - 'order' => 'desc', - 'post_type' => $post_type_filter, - 'post_status' => $post_status_filter, - 'ignore_sticky_posts' => true, - 'no_found_rows' => true, - 'update_post_meta_cache' => false, - 'update_post_term_cache' => false, - 'tax_query' => array( - array( - 'taxonomy' => 'wp_theme', - 'field' => 'name', - 'terms' => $stylesheet, - ), - ), - ); - - $global_style_query = new WP_Query(); - $recent_posts = $global_style_query->query( $args ); - if ( count( $recent_posts ) === 1 ) { - $user_cpt = get_object_vars( $recent_posts[0] ); - } elseif ( $create_post ) { - $cpt_post_id = wp_insert_post( - array( - 'post_content' => '{"version": ' . WP_Theme_JSON::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }', - 'post_status' => 'publish', - 'post_title' => 'Custom Styles', // Do not make string translatable, see https://core.trac.wordpress.org/ticket/54518. - 'post_type' => $post_type_filter, - 'post_name' => sprintf( 'wp-global-styles-%s', urlencode( $stylesheet ) ), - 'tax_input' => array( - 'wp_theme' => array( $stylesheet ), - ), - ), - true - ); - if ( ! is_wp_error( $cpt_post_id ) ) { - $user_cpt = get_object_vars( get_post( $cpt_post_id ) ); - } - } - - return $user_cpt; - } - - /** - * Determines whether the active theme has a theme.json file. - * - * @since 5.8.0 - * @since 5.9.0 Added a check in the parent theme. - * @deprecated 6.2.0 Use wp_theme_has_theme_json() instead. - * - * @return bool - */ - public static function theme_has_support() { - _deprecated_function( __METHOD__, '6.2.0', 'wp_theme_has_theme_json()' ); - - return wp_theme_has_theme_json(); - } - - /** - * Returns the data merged from multiple origins. - * - * There are four sources of data (origins) for a site: - * - * - default => WordPress - * - blocks => each one of the blocks provides data for itself - * - theme => the active theme - * - custom => data provided by the user - * - * The custom's has higher priority than the theme's, the theme's higher than blocks', - * and block's higher than default's. - * - * Unlike the getters - * {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_core_data/ get_core_data}, - * {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_theme_data/ get_theme_data}, - * and {@link https://developer.wordpress.org/reference/classes/wp_theme_json_resolver/get_user_data/ get_user_data}, - * this method returns data after it has been merged with the previous origins. - * This means that if the same piece of data is declared in different origins - * (default, blocks, theme, custom), the last origin overrides the previous. - * - * For example, if the user has set a background color - * for the paragraph block, and the theme has done it as well, - * the user preference wins. - * - * @param string $origin Optional. To what level should we merge data:'default', 'blocks', 'theme' or 'custom'. - * 'custom' is used as default value as well as fallback value if the origin is unknown. - * - * @return WP_Theme_JSON - */ - public static function get_merged_data( $origin = 'custom' ) { - if ( is_array( $origin ) ) { - _deprecated_argument( __FUNCTION__, '5.9.0' ); - } - - $result = static::get_core_data(); - if ( 'default' === $origin ) { - $result->set_spacing_sizes(); - return $result; - } - - $result->merge( static::get_block_data() ); - if ( 'blocks' === $origin ) { - return $result; - } - - $result->merge( static::get_theme_data() ); - if ( 'theme' === $origin ) { - $result->set_spacing_sizes(); - return $result; - } - - $result->merge( static::get_user_data() ); - $result->set_spacing_sizes(); - return $result; - } - - /** - * Returns the user's origin config. - * - * @since 6.2 Added check for the WP_Theme_JSON_Gutenberg class to prevent $user - * values set in core fron overriding the new custom css values added to VALID_STYLES. - * This does not need to be backported to core as the new VALID_STYLES[css] value will - * be added to core with 6.2. - * - * @return WP_Theme_JSON_Gutenberg Entity that holds styles for user data. - */ - public static function get_user_data() { - if ( null !== static::$user && static::$user instanceof WP_Theme_JSON_Gutenberg ) { - return static::$user; - } - - $config = array(); - $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme() ); - - if ( array_key_exists( 'post_content', $user_cpt ) ) { - $decoded_data = json_decode( $user_cpt['post_content'], true ); - - $json_decoding_error = json_last_error(); - if ( JSON_ERROR_NONE !== $json_decoding_error ) { - trigger_error( 'Error when decoding a theme.json schema for user data. ' . json_last_error_msg() ); - /** - * Filters the data provided by the user for global styles & settings. - * - * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data. - */ - $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) ); - $config = $theme_json->get_data(); - return new WP_Theme_JSON_Gutenberg( $config, 'custom' ); - } - - // Very important to verify if the flag isGlobalStylesUserThemeJSON is true. - // If is not true the content was not escaped and is not safe. - if ( - is_array( $decoded_data ) && - isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) && - $decoded_data['isGlobalStylesUserThemeJSON'] - ) { - unset( $decoded_data['isGlobalStylesUserThemeJSON'] ); - $config = $decoded_data; - } - } - - /** - * Filters the data provided by the user for global styles & settings. - * - * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data. - */ - $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) ); - $config = $theme_json->get_data(); - - static::$user = new WP_Theme_JSON_Gutenberg( $config, 'custom' ); - - return static::$user; - } -} diff --git a/lib/compat/wordpress-6.2/get-global-styles-and-settings.php b/lib/compat/wordpress-6.2/get-global-styles-and-settings.php index 8556a0be1663f4..4006b4c23cc0b1 100644 --- a/lib/compat/wordpress-6.2/get-global-styles-and-settings.php +++ b/lib/compat/wordpress-6.2/get-global-styles-and-settings.php @@ -63,9 +63,10 @@ function wp_theme_has_theme_json_clean_cache() { * Returns the stylesheet resulting of merging core, theme, and user data. * * @param array $types Types of styles to load. Optional. - * It accepts 'variables', 'styles', 'presets', 'custom-css' as values. - * If empty, it'll load all for themes with theme.json support - * and only [ 'variables', 'presets' ] for themes without theme.json support. + * It accepts as values: 'variables', 'presets', 'styles', 'base-layout-styles, and 'custom-css'. + * If empty, it'll load the following: + * - for themes without theme.json: 'variables', 'presets', 'base-layout-styles'. + * - for temes with theme.json: 'variables', 'presets', 'styles', 'custom-css'. * * @return string Stylesheet. */ @@ -85,7 +86,7 @@ function gutenberg_get_global_stylesheet( $types = array() ) { if ( empty( $types ) && ! $supports_theme_json ) { $types = array( 'variables', 'presets', 'base-layout-styles' ); } elseif ( empty( $types ) ) { - $types = array( 'variables', 'styles', 'presets', 'custom-css' ); + $types = array( 'variables', 'presets', 'styles', 'custom-css' ); } /* diff --git a/lib/compat/wordpress-6.2/rest-api.php b/lib/compat/wordpress-6.2/rest-api.php index 4900a622b6c01b..12f7afda3b4d5d 100644 --- a/lib/compat/wordpress-6.2/rest-api.php +++ b/lib/compat/wordpress-6.2/rest-api.php @@ -101,3 +101,18 @@ function gutenberg_register_global_styles_endpoints() { $editor_settings->register_routes(); } add_action( 'rest_api_init', 'gutenberg_register_global_styles_endpoints' ); + +/** + * Updates REST API response for the sidebars and marks them as 'inactive'. + * + * Note: This can be a part of the `prepare_item_for_response` in `class-wp-rest-sidebars-controller.php`. + * + * @param WP_REST_Response $response The sidebar response object. + * @return WP_REST_Response $response Updated response object. + */ +function gutenberg_modify_rest_sidebars_response( $response ) { + $response->data['status'] = wp_is_block_theme() ? 'inactive' : 'active'; + + return $response; +} +add_filter( 'rest_prepare_sidebar', 'gutenberg_modify_rest_sidebars_response' ); diff --git a/lib/compat/wordpress-6.2/script-loader.php b/lib/compat/wordpress-6.2/script-loader.php index e08bfa6e46ca8e..e3e11d9cf1f563 100644 --- a/lib/compat/wordpress-6.2/script-loader.php +++ b/lib/compat/wordpress-6.2/script-loader.php @@ -127,6 +127,30 @@ function gutenberg_resolve_assets_override() { $scripts = ob_get_clean(); + /* + * Generate web font @font-face styles for the site editor iframe. + * Use the registered font families for printing. + */ + if ( class_exists( 'WP_Web_Fonts' ) ) { + $wp_webfonts = wp_webfonts(); + $registered = $wp_webfonts->get_registered_font_families(); + if ( ! empty( $registered ) ) { + $queue = $wp_webfonts->queue; + $done = $wp_webfonts->done; + + $wp_webfonts->done = array(); + $wp_webfonts->queue = $registered; + + ob_start(); + $wp_webfonts->do_items(); + $styles .= ob_get_clean(); + + // Reset the Web Fonts API. + $wp_webfonts->done = $done; + $wp_webfonts->queue = $queue; + } + } + return array( 'styles' => $styles, 'scripts' => $scripts, @@ -137,7 +161,8 @@ function gutenberg_resolve_assets_override() { 'block_editor_settings_all', function( $settings ) { // We must override what core is passing now. - $settings['__unstableResolvedAssets'] = gutenberg_resolve_assets_override(); + $settings['__unstableResolvedAssets'] = gutenberg_resolve_assets_override(); + $settings['__unstableIsBlockBasedTheme'] = wp_is_block_theme(); return $settings; }, 100 diff --git a/lib/compat/wordpress-6.2/theme.php b/lib/compat/wordpress-6.2/theme.php new file mode 100644 index 00000000000000..79d55206449472 --- /dev/null +++ b/lib/compat/wordpress-6.2/theme.php @@ -0,0 +1,23 @@ +is_block_theme() ) { + set_theme_mod( 'wp_legacy_sidebars', $wp_registered_sidebars ); + } +} +add_action( 'switch_theme', 'gutenberg_set_legacy_sidebars', 10, 2 ); diff --git a/lib/compat/wordpress-6.2/widgets.php b/lib/compat/wordpress-6.2/widgets.php new file mode 100644 index 00000000000000..19591ae64607e3 --- /dev/null +++ b/lib/compat/wordpress-6.2/widgets.php @@ -0,0 +1,32 @@ + array( 'post-editor', 'site-editor', 'widgets-editor' ), ), + '__experimentalBlockInspectorAnimation' => array( + 'description' => __( 'Whether to enable animation when showing and hiding the block inspector.', 'gutenberg' ), + 'type' => 'object', + 'context' => array( 'site-editor' ), + ), + 'alignWide' => array( 'description' => __( 'Enable/Disable Wide/Full Alignments.', 'gutenberg' ), 'type' => 'boolean', diff --git a/lib/experimental/class-wp-theme-json-gutenberg.php b/lib/experimental/class-wp-theme-json-gutenberg.php deleted file mode 100644 index a16697dff07d07..00000000000000 --- a/lib/experimental/class-wp-theme-json-gutenberg.php +++ /dev/null @@ -1,31 +0,0 @@ - true ) ) { - if ( ! empty( $deprecated ) ) { - _deprecated_argument( __METHOD__, '5.9' ); - } - - // When backporting to core, remove the instanceof Gutenberg class check, as it is only required for the Gutenberg plugin. - if ( null === static::$theme || ! static::has_same_registered_blocks( 'theme' ) ) { - $theme_json_file = static::get_file_path_from_theme( 'theme.json' ); - $wp_theme = wp_get_theme(); - if ( '' !== $theme_json_file ) { - $theme_json_data = static::read_json_file( $theme_json_file ); - $theme_json_data = static::translate( $theme_json_data, $wp_theme->get( 'TextDomain' ) ); - } else { - $theme_json_data = array(); - } - $theme_json_data = gutenberg_add_registered_webfonts_to_theme_json( $theme_json_data ); - - /** - * Filters the data provided by the theme for global styles & settings. - * - * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data. - */ - $theme_json = apply_filters( 'wp_theme_json_data_theme', new WP_Theme_JSON_Data_Gutenberg( $theme_json_data, 'theme' ) ); - $theme_json_data = $theme_json->get_data(); - static::$theme = new WP_Theme_JSON_Gutenberg( $theme_json_data ); - - if ( $wp_theme->parent() ) { - // Get parent theme.json. - $parent_theme_json_file = static::get_file_path_from_theme( 'theme.json', true ); - if ( '' !== $parent_theme_json_file ) { - $parent_theme_json_data = static::read_json_file( $parent_theme_json_file ); - $parent_theme_json_data = gutenberg_add_registered_webfonts_to_theme_json( $parent_theme_json_data ); - $parent_theme = new WP_Theme_JSON_Gutenberg( $parent_theme_json_data ); - - /* - * Merge the child theme.json into the parent theme.json. - * The child theme takes precedence over the parent. - */ - $parent_theme->merge( static::$theme ); - static::$theme = $parent_theme; - } - } - } - - if ( ! $settings['with_supports'] ) { - return static::$theme; - } - - /* - * We want the presets and settings declared in theme.json - * to override the ones declared via theme supports. - * So we take theme supports, transform it to theme.json shape - * and merge the static::$theme upon that. - */ - $theme_support_data = WP_Theme_JSON_Gutenberg::get_from_editor_settings( gutenberg_get_legacy_theme_supports_for_theme_json() ); - if ( ! wp_theme_has_theme_json() ) { - if ( ! isset( $theme_support_data['settings']['color'] ) ) { - $theme_support_data['settings']['color'] = array(); - } - - $default_palette = false; - if ( current_theme_supports( 'default-color-palette' ) ) { - $default_palette = true; - } - if ( ! isset( $theme_support_data['settings']['color']['palette'] ) ) { - // If the theme does not have any palette, we still want to show the core one. - $default_palette = true; - } - $theme_support_data['settings']['color']['defaultPalette'] = $default_palette; - - $default_gradients = false; - if ( current_theme_supports( 'default-gradient-presets' ) ) { - $default_gradients = true; - } - if ( ! isset( $theme_support_data['settings']['color']['gradients'] ) ) { - // If the theme does not have any gradients, we still want to show the core ones. - $default_gradients = true; - } - $theme_support_data['settings']['color']['defaultGradients'] = $default_gradients; - - // Classic themes without a theme.json don't support global duotone. - $theme_support_data['settings']['color']['defaultDuotone'] = false; - - // Allow themes to enable appearance tools via theme_support. - if ( current_theme_supports( 'appearance-tools' ) ) { - $theme_support_data['settings']['appearanceTools'] = true; - } - } - $with_theme_supports = new WP_Theme_JSON_Gutenberg( $theme_support_data ); - $with_theme_supports->merge( static::$theme ); - - return $with_theme_supports; - } - - /** - * Gets the styles for blocks from the block.json file. - * - * @return WP_Theme_JSON - */ - public static function get_block_data() { - $registry = WP_Block_Type_Registry::get_instance(); - $blocks = $registry->get_all_registered(); - $config = array( 'version' => 1 ); - foreach ( $blocks as $block_name => $block_type ) { - if ( isset( $block_type->supports['__experimentalStyle'] ) ) { - $config['styles']['blocks'][ $block_name ] = static::remove_JSON_comments( $block_type->supports['__experimentalStyle'] ); - } - - if ( - isset( $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ) && - null === _wp_array_get( $config, array( 'styles', 'blocks', $block_name, 'spacing', 'blockGap' ), null ) - ) { - // Ensure an empty placeholder value exists for the block, if it provides a default blockGap value. - // The real blockGap value to be used will be determined when the styles are rendered for output. - $config['styles']['blocks'][ $block_name ]['spacing']['blockGap'] = null; - } - } - - /** - * Filters the data provided by the blocks for global styles & settings. - * - * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data. - */ - $theme_json = apply_filters( 'wp_theme_json_data_blocks', new WP_Theme_JSON_Data_Gutenberg( $config, 'blocks' ) ); - $config = $theme_json->get_data(); - - return new WP_Theme_JSON_Gutenberg( $config, 'blocks' ); - } - - /** - * When given an array, this will remove any keys with the name `//`. - * - * @param array $array The array to filter. - * @return array The filtered array. - */ - private static function remove_JSON_comments( $array ) { - unset( $array['//'] ); - foreach ( $array as $k => $v ) { - if ( is_array( $v ) ) { - $array[ $k ] = static::remove_JSON_comments( $v ); - } - } - - return $array; - } - -} diff --git a/lib/experimental/class-wp-web-fonts.php b/lib/experimental/class-wp-web-fonts.php new file mode 100644 index 00000000000000..15df7ecf47e5c5 --- /dev/null +++ b/lib/experimental/class-wp-web-fonts.php @@ -0,0 +1,744 @@ + 'local', + 'font-family' => '', + 'font-style' => 'normal', + 'font-weight' => '400', + 'font-display' => 'fallback', + ); + + /** + * Constructor. + * + * @since X.X.X + */ + public function __construct() { + /** + * Filters the web font variation's property defaults. + * + * @since X.X.X + * + * @param array $defaults { + * An array of required web font properties and defaults. + * + * @type string $provider The provider ID. Default 'local'. + * @type string $font-family The font-family property. Default empty string. + * @type string $font-style The font-style property. Default 'normal'. + * @type string $font-weight The font-weight property. Default '400'. + * @type string $font-display The font-display property. Default 'fallback'. + * } + */ + $this->variation_property_defaults = apply_filters( 'wp_webfont_variation_defaults', $this->variation_property_defaults ); + + /** + * Fires when the WP_Webfonts instance is initialized. + * + * @since X.X.X + * + * @param WP_Web_Fonts $wp_webfonts WP_Web_Fonts instance (passed by reference). + */ + do_action_ref_array( 'wp_default_webfonts', array( &$this ) ); + } + + /** + * Get the list of registered providers. + * + * @since X.X.X + * + * @return array $providers { + * An associative array of registered providers, keyed by their unique ID. + * + * @type string $provider_id => array { + * An associate array of provider's class name and fonts. + * + * @type string $class Fully qualified name of the provider's class. + * @type string[] $fonts An array of enqueued font handles for this provider. + * } + * } + */ + public function get_providers() { + return $this->providers; + } + + /** + * Register a provider. + * + * @since X.X.X + * + * @param string $provider_id The provider's unique ID. + * @param string $class The provider class name. + * @return bool True if successfully registered, else false. + */ + public function register_provider( $provider_id, $class ) { + if ( empty( $provider_id ) || empty( $class ) || ! class_exists( $class ) ) { + return false; + } + + $this->providers[ $provider_id ] = array( + 'class' => $class, + 'fonts' => array(), + ); + return true; + } + + /** + * Get the list of all registered font family handles. + * + * @since X.X.X + * + * @return string[] + */ + public function get_registered_font_families() { + $font_families = array(); + foreach ( $this->registered as $handle => $obj ) { + if ( $obj->extra['is_font_family'] ) { + $font_families[] = $handle; + } + } + return $font_families; + } + + /** + * Get the list of all registered font families and their variations. + * + * @since X.X.X + * + * @return string[] + */ + public function get_registered() { + return array_keys( $this->registered ); + } + + /** + * Get the list of enqueued font families and their variations. + * + * @since X.X.X + * + * @return array[] + */ + public function get_enqueued() { + return $this->queue; + } + + /** + * Registers a font family. + * + * @since X.X.X + * + * @param string $font_family Font family name to register. + * @return string|null Font family handle when registration successes. Null on failure. + */ + public function add_font_family( $font_family ) { + $font_family_handle = WP_Webfonts_Utils::convert_font_family_into_handle( $font_family ); + if ( ! $font_family_handle ) { + return null; + } + + if ( isset( $this->registered[ $font_family_handle ] ) ) { + return $font_family_handle; + } + + $registered = $this->add( $font_family_handle, false ); + if ( ! $registered ) { + return null; + } + + $this->add_data( $font_family_handle, 'font-properties', array( 'font-family' => $font_family ) ); + $this->add_data( $font_family_handle, 'is_font_family', true ); + + return $font_family_handle; + } + + /** + * Removes a font family and all registered variations. + * + * @since X.X.X + * + * @param string $font_family_handle The font family to remove. + */ + public function remove_font_family( $font_family_handle ) { + if ( ! isset( $this->registered[ $font_family_handle ] ) ) { + return; + } + + $variations = $this->registered[ $font_family_handle ]->deps; + + foreach ( $variations as $variation ) { + $this->remove( $variation ); + } + + $this->remove( $font_family_handle ); + } + + /** + * Add a variation to an existing family or register family if none exists. + * + * @since X.X.X + * + * @param string $font_family_handle The font family's handle for this variation. + * @param array $variation An array of variation properties to add. + * @param string $variation_handle Optional. The variation's handle. When none is provided, the + * handle will be dynamically generated. + * Default empty string. + * @return string|null Variation handle on success. Else null. + */ + public function add_variation( $font_family_handle, array $variation, $variation_handle = '' ) { + if ( ! WP_Webfonts_Utils::is_defined( $font_family_handle ) ) { + trigger_error( 'Font family handle must be a non-empty string.' ); + return null; + } + + // When there is a variation handle, check it. + if ( '' !== $variation_handle && ! WP_Webfonts_Utils::is_defined( $variation_handle ) ) { + trigger_error( 'Variant handle must be a non-empty string.' ); + return null; + } + + // Register the font family when it does not yet exist. + if ( ! isset( $this->registered[ $font_family_handle ] ) ) { + if ( ! $this->add_font_family( $font_family_handle ) ) { + return null; + } + } + + $variation = $this->validate_variation( $variation ); + + // Variation validation failed. + if ( ! $variation ) { + return null; + } + + // When there's no variation handle, attempt to create one. + if ( '' === $variation_handle ) { + $variation_handle = WP_Webfonts_Utils::convert_variation_into_handle( $font_family_handle, $variation ); + if ( is_null( $variation_handle ) ) { + return null; + } + } + + // Bail out if the variant is already registered. + if ( $this->is_variation_registered( $font_family_handle, $variation_handle ) ) { + return $variation_handle; + } + + $variation_src = array_key_exists( 'src', $variation ) ? $variation['src'] : false; + $result = $this->add( $variation_handle, $variation_src ); + + // Bail out if the registration failed. + if ( ! $result ) { + return null; + } + + $this->add_data( $variation_handle, 'font-properties', $variation ); + $this->add_data( $variation_handle, 'is_font_family', false ); + + // Add the font variation as a dependency to the registered font family. + $this->add_dependency( $font_family_handle, $variation_handle ); + + $this->providers[ $variation['provider'] ]['fonts'][] = $variation_handle; + + return $variation_handle; + } + + /** + * Removes a variation. + * + * @since X.X.X + * + * @param string $font_family_handle The font family for this variation. + * @param string $variation_handle The variation's handle to remove. + */ + public function remove_variation( $font_family_handle, $variation_handle ) { + if ( isset( $this->registered[ $variation_handle ] ) ) { + $this->remove( $variation_handle ); + } + + if ( ! $this->is_variation_registered( $font_family_handle, $variation_handle ) ) { + return; + } + + // Remove the variation as a dependency from its font family. + $this->registered[ $font_family_handle ]->deps = array_values( + array_diff( + $this->registered[ $font_family_handle ]->deps, + array( $variation_handle ) + ) + ); + } + + /** + * Checks if the variation is registered. + * + * @since X.X.X + * + * @param string $font_family_handle The font family's handle for this variation. + * @param string $variation_handle Variation's handle. + * @return bool True when registered to the given font family. Else false. + */ + private function is_variation_registered( $font_family_handle, $variation_handle ) { + if ( ! isset( $this->registered[ $font_family_handle ] ) ) { + return false; + } + + return in_array( $variation_handle, $this->registered[ $font_family_handle ]->deps, true ); + } + + /** + * Adds a variation as a dependency to the given font family. + * + * @since X.X.X + * + * @param string $font_family_handle The font family's handle for this variation. + * @param string $variation_handle The variation's handle. + */ + private function add_dependency( $font_family_handle, $variation_handle ) { + $this->registered[ $font_family_handle ]->deps[] = $variation_handle; + } + + /** + * Validates and sanitizes a variation. + * + * @since X.X.X + * + * @param array $variation Variation properties to add. + * @return false|array Validated variation on success. Else, false. + */ + private function validate_variation( $variation ) { + $variation = wp_parse_args( $variation, $this->variation_property_defaults ); + + // Check the font-family. + if ( empty( $variation['font-family'] ) || ! is_string( $variation['font-family'] ) ) { + trigger_error( 'Webfont font-family must be a non-empty string.' ); + return false; + } + + // Local fonts need a "src". + if ( 'local' === $variation['provider'] ) { + // Make sure that local fonts have 'src' defined. + if ( empty( $variation['src'] ) || ( ! is_string( $variation['src'] ) && ! is_array( $variation['src'] ) ) ) { + trigger_error( 'Webfont src must be a non-empty string or an array of strings.' ); + return false; + } + } elseif ( ! isset( $this->providers[ $variation['provider'] ] ) ) { + trigger_error( sprintf( 'The provider "%s" is not registered', $variation['provider'] ) ); + return false; + } elseif ( ! class_exists( $this->providers[ $variation['provider'] ]['class'] ) ) { + trigger_error( sprintf( 'The provider class "%s" does not exist', $variation['provider'] ) ); + return false; + } + + // Validate the 'src' property. + if ( ! empty( $variation['src'] ) ) { + foreach ( (array) $variation['src'] as $src ) { + if ( empty( $src ) || ! is_string( $src ) ) { + trigger_error( 'Each webfont src must be a non-empty string.' ); + return false; + } + } + } + + // Check the font-weight. + if ( ! is_string( $variation['font-weight'] ) && ! is_int( $variation['font-weight'] ) ) { + trigger_error( 'Webfont font-weight must be a properly formatted string or integer.' ); + return false; + } + + // Check the font-display. + if ( ! in_array( $variation['font-display'], array( 'auto', 'block', 'fallback', 'swap', 'optional' ), true ) ) { + $variation['font-display'] = 'fallback'; + } + + $valid_props = array( + 'ascent-override', + 'descent-override', + 'font-display', + 'font-family', + 'font-stretch', + 'font-style', + 'font-weight', + 'font-variant', + 'font-feature-settings', + 'font-variation-settings', + 'line-gap-override', + 'size-adjust', + 'src', + 'unicode-range', + + // Exceptions. + 'provider', + ); + + foreach ( $variation as $prop => $value ) { + if ( ! in_array( $prop, $valid_props, true ) ) { + unset( $variation[ $prop ] ); + } + } + + return $variation; + } + + /** + * Processes the items and dependencies. + * + * Processes the items passed to it or the queue, and their dependencies. + * + * @since X.X.X + * + * @param string|string[]|bool $handles Optional. Items to be processed: queue (false), + * single item (string), or multiple items (array of strings). + * Default false. + * @param int|false $group Optional. Group level: level (int), no group (false). + * + * @return array|string[] Array of web font handles that have been processed. + * An empty array if none were processed. + */ + public function do_items( $handles = false, $group = false ) { + $handles = $this->prepare_handles_for_printing( $handles ); + + if ( empty( $handles ) ) { + return $this->done; + } + + $this->all_deps( $handles ); + if ( empty( $this->to_do ) ) { + return $this->done; + } + + $this->to_do_keyed_handles = array_flip( $this->to_do ); + + foreach ( $this->get_providers() as $provider_id => $provider ) { + // Alert and skip if the provider class does not exist. + if ( ! class_exists( $provider['class'] ) ) { + /* translators: %s is the provider name. */ + trigger_error( + sprintf( + 'Class "%s" not found for "%s" web font provider', + $provider['class'], + $provider_id + ) + ); + continue; + } + + $this->do_item( $provider_id, $group ); + } + + $this->process_font_families_after_printing( $handles ); + + return $this->done; + } + + /** + * Prepares the given handles for printing. + * + * @since X.X.X + * + * @param string|string[]|bool $handles Optional. Handles to prepare. + * Default false. + * @return array Array of handles. + */ + private function prepare_handles_for_printing( $handles = false ) { + if ( false !== $handles ) { + $handles = $this->validate_handles( $handles ); + // Bail out when invalid. + if ( empty( $handles ) ) { + return array(); + } + } + + // Use the enqueued queue. + if ( empty( $handles ) ) { + if ( empty( $this->queue ) ) { + return array(); + } + $handles = $this->queue; + } + + return $handles; + } + + /** + * Validates handle(s) to ensure each is a non-empty string. + * + * @since X.X.X + * + * @param string|string[] $handles Handles to prepare. + * @return string[]|null Array of handles on success. Else null. + */ + private function validate_handles( $handles ) { + // Validate each element is a non-empty string handle. + $handles = array_filter( (array) $handles, array( WP_Webfonts_Utils::class, 'is_defined' ) ); + + if ( empty( $handles ) ) { + trigger_error( 'Handles must be a non-empty string or array of non-empty strings' ); + return null; + } + + return $handles; + } + + /** + * Invokes each provider to process and print its styles. + * + * @since X.X.X + * + * @see WP_Dependencies::do_item() + * + * @param string $provider_id The provider to process. + * @param int|false $group Not used. + * @return bool + */ + public function do_item( $provider_id, $group = false ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + // Bail out if the provider is not registered. + if ( ! isset( $this->providers[ $provider_id ] ) ) { + return false; + } + + $font_handles = $this->get_enqueued_fonts_for_provider( $provider_id ); + if ( empty( $font_handles ) ) { + return false; + } + + $properties_by_font = $this->get_font_properties_for_provider( $font_handles ); + if ( empty( $properties_by_font ) ) { + return false; + } + + // Invoke provider to print its styles. + $provider = $this->get_provider_instance( $provider_id ); + $provider->set_webfonts( $properties_by_font ); + $provider->print_styles(); + + // Clean up. + $this->update_queues_for_printed_fonts( $font_handles ); + + return true; + } + + /** + * Retrieves a list of enqueued web font variations for a provider. + * + * @since X.X.X + * + * @param string $provider_id The provider to process. + * @return array[] Webfonts organized by providers. + */ + private function get_enqueued_fonts_for_provider( $provider_id ) { + $providers = $this->get_providers(); + + if ( empty( $providers[ $provider_id ] ) ) { + return array(); + } + + return array_intersect( + $providers[ $provider_id ]['fonts'], + $this->to_do + ); + } + + /** + * Gets a list of font properties for each of the given font handles. + * + * @since X.X.X + * + * @param array $font_handles Font handles to get properties. + * @return array A list of fonts with each font's properties. + */ + private function get_font_properties_for_provider( array $font_handles ) { + $font_properties = array(); + + foreach ( $font_handles as $font_handle ) { + $properties = $this->get_data( $font_handle, 'font-properties' ); + if ( ! $properties ) { + continue; + } + $font_properties[ $font_handle ] = $properties; + } + + return $font_properties; + } + + /** + * Gets the instance of the provider from the WP_Webfonts::$provider_instance store. + * + * @since X.X.X + * + * @param string $provider_id The provider to get. + * @return object Instance of the provider. + */ + private function get_provider_instance( $provider_id ) { + if ( ! isset( $this->provider_instances[ $provider_id ] ) ) { + $this->provider_instances[ $provider_id ] = new $this->providers[ $provider_id ]['class'](); + } + return $this->provider_instances[ $provider_id ]; + } + + /** + * Update queues for the given printed fonts. + * + * @since X.X.X + * + * @param array $font_handles Font handles to get properties. + */ + private function update_queues_for_printed_fonts( array $font_handles ) { + foreach ( $font_handles as $font_handle ) { + $this->set_as_done( $font_handle ); + $this->remove_from_to_do_queues( $font_handle ); + } + } + + /** + * Processes the font families after printing the variations. + * + * For each queued font family: + * + * a. if any of their variations were printed, the font family is added to the `done` list. + * b. removes each from the to_do queues. + * + * @since X.X.X + * + * @param array $handles Handles to process. + */ + private function process_font_families_after_printing( array $handles ) { + foreach ( $handles as $handle ) { + if ( + ! $this->get_data( $handle, 'is_font_family' ) || + ! isset( $this->to_do_keyed_handles[ $handle ] ) + ) { + continue; + } + $font_family = $this->registered[ $handle ]; + + // Add the font family to `done` list if any of its variations were printed. + if ( ! empty( $font_family->deps ) ) { + $processed = array_intersect( $font_family->deps, $this->done ); + if ( ! empty( $processed ) ) { + $this->set_as_done( $handle ); + } + } + + $this->remove_from_to_do_queues( $handle ); + } + } + + /** + * Removes the handle from the `to_do` and `to_do_keyed_handles` lists. + * + * @since X.X.X + * + * @param string $handle Handle to remove. + */ + private function remove_from_to_do_queues( $handle ) { + unset( + $this->to_do[ $this->to_do_keyed_handles[ $handle ] ], + $this->to_do_keyed_handles[ $handle ] + ); + } + + /** + * Sets the given handle to done by adding it to the `done` list. + * + * @since X.X.X + * + * @param string $handle Handle to set as done. + */ + private function set_as_done( $handle ) { + if ( ! is_array( $this->done ) ) { + $this->done = array(); + } + $this->done[] = $handle; + } + + /** + * Converts the font family and its variations into theme.json structural format. + * + * @since X.X.X + * + * @param string $font_family_handle Font family to convert. + * @return array Webfonts in theme.json structural format. + */ + public function to_theme_json( $font_family_handle ) { + if ( ! isset( $this->registered[ $font_family_handle ] ) ) { + return array(); + } + + $font_family_name = $this->registered[ $font_family_handle ]->extra['font-properties']['font-family']; + $theme_json_format = array( + 'fontFamily' => str_contains( $font_family_name, ' ' ) ? "'{$font_family_name}'" : $font_family_name, + 'name' => $font_family_name, + 'slug' => $font_family_handle, + 'fontFace' => array(), + ); + + foreach ( $this->registered[ $font_family_handle ]->deps as $variation_handle ) { + if ( ! isset( $this->registered[ $variation_handle ] ) ) { + continue; + } + + $variation_obj = $this->registered[ $variation_handle ]; + $variation_properties = array( 'origin' => 'gutenberg_wp_webfonts_api' ); + foreach ( $variation_obj->extra['font-properties'] as $property_name => $property_value ) { + $property_in_camelcase = lcfirst( str_replace( '-', '', ucwords( $property_name, '-' ) ) ); + $variation_properties[ $property_in_camelcase ] = $property_value; + } + $theme_json_format['fontFace'][ $variation_obj->handle ] = $variation_properties; + } + + return $theme_json_format; + } +} diff --git a/lib/experimental/class-wp-webfonts-provider-local.php b/lib/experimental/class-wp-webfonts-provider-local.php index 9af27fa7c436a7..3950ebcd624c39 100644 --- a/lib/experimental/class-wp-webfonts-provider-local.php +++ b/lib/experimental/class-wp-webfonts-provider-local.php @@ -35,6 +35,21 @@ class WP_Webfonts_Provider_Local extends WP_Webfonts_Provider { */ protected $id = 'local'; + /** + * Constructor. + * + * @since 6.1.0 + */ + public function __construct() { + if ( + function_exists( 'is_admin' ) && ! is_admin() + && + function_exists( 'current_theme_supports' ) && ! current_theme_supports( 'html5', 'style' ) + ) { + $this->style_tag_atts = array( 'type' => 'text/css' ); + } + } + /** * Gets the `@font-face` CSS styles for locally-hosted font files. * @@ -81,7 +96,7 @@ class WP_Webfonts_Provider_Local extends WP_Webfonts_Provider { * } * * - * @since 6.0.0 + * @since X.X.X * * @return string The `@font-face` CSS. */ @@ -183,7 +198,8 @@ private function order_src( array $webfont ) { private function build_font_face_css( array $webfont ) { $css = ''; - // Wrap font-family in quotes if it contains spaces. + // Wrap font-family in quotes if it contains spaces + // and is not already wrapped in quotes. if ( str_contains( $webfont['font-family'], ' ' ) && ! str_contains( $webfont['font-family'], '"' ) && diff --git a/lib/experimental/class-wp-webfonts-provider.php b/lib/experimental/class-wp-webfonts-provider.php index a49b8a324b7f5f..8fcbd76a76d3cf 100644 --- a/lib/experimental/class-wp-webfonts-provider.php +++ b/lib/experimental/class-wp-webfonts-provider.php @@ -6,7 +6,7 @@ * * @package WordPress * @subpackage WebFonts - * @since 6.0.0 + * @since X.X.X */ if ( class_exists( 'WP_Webfonts_Provider' ) ) { @@ -30,26 +30,46 @@ * into styles (in a performant way for the provider service * it manages). * - * @since 6.0.0 + * @since X.X.X */ abstract class WP_Webfonts_Provider { + /** + * The provider's unique ID. + * + * @since X.X.X + * + * @var string + */ + protected $id = ''; + /** * Webfonts to be processed. * - * @since 6.0.0 + * @since X.X.X * * @var array[] */ protected $webfonts = array(); + /** + * Array of Font-face style tag's attribute(s) + * where the key is the attribute name and the + * value is its value. + * + * @since X.X.X + * + * @var string[] + */ + protected $style_tag_atts = array(); + /** * Sets this provider's webfonts property. * * The API's Controller passes this provider's webfonts * for processing here in the provider. * - * @since 6.0.0 + * @since X.X.X * * @param array[] $webfonts Registered webfonts. */ @@ -57,6 +77,18 @@ public function set_webfonts( array $webfonts ) { $this->webfonts = $webfonts; } + /** + * Prints the generated styles. + * + * @since X.X.X + */ + public function print_styles() { + printf( + $this->get_style_element(), + $this->get_css() + ); + } + /** * Gets the `@font-face` CSS for the provider's webfonts. * @@ -64,9 +96,37 @@ public function set_webfonts( array $webfonts ) { * needed `@font-face` CSS for all of its webfonts. Specifics of how * this processing is done is contained in each provider. * - * @since 6.0.0 + * @since X.X.X * * @return string The `@font-face` CSS. */ abstract public function get_css(); + + /** + * Gets the `\n"; + } + + /** + * Gets the defined - { styles.map( + { [ ...styles, ...neededCompatStyles ].map( ( { tagName, href, id, rel, media, textContent } ) => { const TagName = tagName.toLowerCase(); @@ -342,7 +256,7 @@ function Iframe( ref={ bodyRef } className={ classnames( 'block-editor-iframe__body', - BODY_CLASS_NAME, + 'editor-styles-wrapper', ...bodyClasses ) } style={ { @@ -359,15 +273,6 @@ function Iframe( inert={ readonly ? 'true' : undefined } > { contentResizeListener } - { /* - * This is a wrapper for the extra styles and scripts - * rendered imperatively by cloning the parent, - * it's important that this div's content remains uncontrolled. - */ } -
{ children } diff --git a/packages/block-editor/src/components/iframe/use-compatibility-styles.js b/packages/block-editor/src/components/iframe/use-compatibility-styles.js new file mode 100644 index 00000000000000..5a1d7de5518654 --- /dev/null +++ b/packages/block-editor/src/components/iframe/use-compatibility-styles.js @@ -0,0 +1,95 @@ +/** + * WordPress dependencies + */ +import { useMemo } from '@wordpress/element'; + +/** + * Returns a list of stylesheets that target the editor canvas. A stylesheet is + * considered targetting the editor a canvas if it contains the + * `editor-styles-wrapper`, `wp-block`, or `wp-block-*` class selectors. + * + * Ideally, this hook should be removed in the future and styles should be added + * explicitly as editor styles. + */ +export function useCompatibilityStyles() { + // Only memoize the result once on load, since these stylesheets should not + // change. + return useMemo( () => { + // Search the document for stylesheets targetting the editor canvas. + return Array.from( document.styleSheets ).reduce( + ( accumulator, styleSheet ) => { + try { + // May fail for external styles. + // eslint-disable-next-line no-unused-expressions + styleSheet.cssRules; + } catch ( e ) { + return accumulator; + } + + const { ownerNode, cssRules } = styleSheet; + + if ( ! cssRules ) { + return accumulator; + } + + // Generally, ignore inline styles. We add inline styles belonging to a + // stylesheet later, which may or may not match the selectors. + if ( ownerNode.tagName !== 'LINK' ) { + return accumulator; + } + + // Don't try to add the reset styles, which were removed as a dependency + // from `edit-blocks` for the iframe since we don't need to reset admin + // styles. + if ( ownerNode.id === 'wp-reset-editor-styles-css' ) { + return accumulator; + } + + function matchFromRules( _cssRules ) { + return Array.from( _cssRules ).find( + ( { + selectorText, + conditionText, + cssRules: __cssRules, + } ) => { + // If the rule is conditional then it will not have selector text. + // Recurse into child CSS ruleset to determine selector eligibility. + if ( conditionText ) { + return matchFromRules( __cssRules ); + } + + return ( + selectorText && + ( selectorText.includes( + '.editor-styles-wrapper' + ) || + selectorText.includes( '.wp-block' ) ) + ); + } + ); + } + + if ( matchFromRules( cssRules ) ) { + // Display warning once we have a way to add style dependencies to the editor. + // See: https://github.com/WordPress/gutenberg/pull/37466. + accumulator.push( ownerNode.cloneNode( true ) ); + + // Add inline styles belonging to the stylesheet. + const inlineCssId = ownerNode.id.replace( + '-css', + '-inline-css' + ); + const inlineCssElement = + document.getElementById( inlineCssId ); + + if ( inlineCssElement ) { + accumulator.push( inlineCssElement.cloneNode( true ) ); + } + } + + return accumulator; + }, + [] + ); + }, [] ); +} diff --git a/packages/block-editor/src/components/image-size-control/test/index.js b/packages/block-editor/src/components/image-size-control/test/index.js index e265d07eebc1c3..3033e07147e394 100644 --- a/packages/block-editor/src/components/image-size-control/test/index.js +++ b/packages/block-editor/src/components/image-size-control/test/index.js @@ -1,7 +1,8 @@ /** * External dependencies */ -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; /** * Internal dependencies @@ -11,8 +12,6 @@ import ImageSizeControl from '../index'; describe( 'ImageSizeControl', () => { const mockOnChange = jest.fn(); const mockOnChangeImage = jest.fn(); - const getHeightInput = () => screen.getByLabelText( 'Height' ); - const getWidthInput = () => screen.getByLabelText( 'Width' ); afterEach( () => { // Cleanup on exiting. @@ -31,8 +30,12 @@ describe( 'ImageSizeControl', () => { /> ); - expect( getHeightInput().value ).toBe( '300' ); - expect( getWidthInput().value ).toBe( '400' ); + expect( + screen.getByRole( 'spinbutton', { name: 'Height' } ) + ).toHaveValue( 300 ); + expect( + screen.getByRole( 'spinbutton', { name: 'Width' } ) + ).toHaveValue( 400 ); } ); it( 'returns default dimensions when custom dimensions are undefined', () => { @@ -44,15 +47,23 @@ describe( 'ImageSizeControl', () => { /> ); - expect( getHeightInput().value ).toBe( '100' ); - expect( getWidthInput().value ).toBe( '200' ); + expect( + screen.getByRole( 'spinbutton', { name: 'Height' } ) + ).toHaveValue( 100 ); + expect( + screen.getByRole( 'spinbutton', { name: 'Width' } ) + ).toHaveValue( 200 ); } ); - it( 'returns empty string when custom and default dimensions are undefined', () => { + it( 'returns no value when custom and default dimensions are undefined', () => { render( ); - expect( getHeightInput().value ).toBe( '' ); - expect( getWidthInput().value ).toBe( '' ); + expect( + screen.getByRole( 'spinbutton', { name: 'Height' } ) + ).toHaveValue( null ); + expect( + screen.getByRole( 'spinbutton', { name: 'Width' } ) + ).toHaveValue( null ); } ); it( 'returns default dimensions when initially undefined defaults are defined on rerender', () => { @@ -62,12 +73,14 @@ describe( 'ImageSizeControl', () => { ); - const heightInput = getHeightInput(); - const widthInput = getWidthInput(); + const heightInput = screen.getByRole( 'spinbutton', { + name: 'Height', + } ); + const widthInput = screen.getByRole( 'spinbutton', { name: 'Width' } ); // The dimensions are initially set to an empty string. - expect( heightInput.value ).toBe( '' ); - expect( widthInput.value ).toBe( '' ); + expect( heightInput ).toHaveValue( null ); + expect( widthInput ).toHaveValue( null ); // When new default dimensions are passed on a rerender (for example after they // are calculated following an image upload), update values to the new defaults. @@ -80,48 +93,66 @@ describe( 'ImageSizeControl', () => { ); // The dimensions should update to the defaults. - expect( heightInput.value ).toBe( '300' ); - expect( widthInput.value ).toBe( '400' ); + expect( heightInput ).toHaveValue( 300 ); + expect( widthInput ).toHaveValue( 400 ); } ); describe( 'updating dimension inputs', () => { - it( 'updates height and calls onChange', () => { + it( 'updates height and calls onChange', async () => { + const user = userEvent.setup(); render( ); - const heightInput = getHeightInput(); - const widthInput = getWidthInput(); + const heightInput = screen.getByRole( 'spinbutton', { + name: 'Height', + } ); + const widthInput = screen.getByRole( 'spinbutton', { + name: 'Width', + } ); - expect( heightInput.value ).toBe( '' ); - expect( widthInput.value ).toBe( '' ); + expect( heightInput ).toHaveValue( null ); + expect( widthInput ).toHaveValue( null ); - fireEvent.change( heightInput, { target: { value: '500' } } ); + const newHeight = '500'; - expect( mockOnChange ).toHaveBeenCalledTimes( 1 ); - expect( mockOnChange ).toHaveBeenCalledWith( { height: 500 } ); + await user.clear( heightInput ); + await user.type( heightInput, newHeight ); - expect( heightInput.value ).toBe( '500' ); - expect( widthInput.value ).toBe( '' ); + expect( mockOnChange ).toHaveBeenCalledTimes( newHeight.length ); + expect( mockOnChange ).toHaveBeenLastCalledWith( { height: 500 } ); + + expect( heightInput ).toHaveValue( 500 ); + expect( widthInput ).toHaveValue( null ); } ); - it( 'updates width and calls onChange', () => { + it( 'updates width and calls onChange', async () => { + const user = userEvent.setup(); + render( ); - const heightInput = getHeightInput(); - const widthInput = getWidthInput(); + const heightInput = screen.getByRole( 'spinbutton', { + name: 'Height', + } ); + const widthInput = screen.getByRole( 'spinbutton', { + name: 'Width', + } ); - expect( heightInput.value ).toBe( '' ); - expect( widthInput.value ).toBe( '' ); + expect( heightInput ).toHaveValue( null ); + expect( widthInput ).toHaveValue( null ); - fireEvent.change( widthInput, { target: { value: '500' } } ); + const newWidth = '500'; + await user.clear( widthInput ); + await user.type( widthInput, newWidth ); - expect( mockOnChange ).toHaveBeenCalledTimes( 1 ); - expect( mockOnChange ).toHaveBeenCalledWith( { width: 500 } ); + expect( mockOnChange ).toHaveBeenCalledTimes( newWidth.length ); + expect( mockOnChange ).toHaveBeenLastCalledWith( { width: 500 } ); - expect( heightInput.value ).toBe( '' ); - expect( widthInput.value ).toBe( '500' ); + expect( heightInput ).toHaveValue( null ); + expect( widthInput ).toHaveValue( 500 ); } ); - it( 'updates height and calls onChange for empty value', () => { + it( 'updates height and calls onChange for empty value', async () => { + const user = userEvent.setup(); + render( { /> ); - const heightInput = getHeightInput(); - const widthInput = getWidthInput(); + const heightInput = screen.getByRole( 'spinbutton', { + name: 'Height', + } ); + const widthInput = screen.getByRole( 'spinbutton', { + name: 'Width', + } ); - expect( heightInput.value ).toBe( '100' ); - expect( widthInput.value ).toBe( '100' ); + expect( heightInput ).toHaveValue( 100 ); + expect( widthInput ).toHaveValue( 100 ); - fireEvent.change( heightInput, { target: { value: '' } } ); + await user.clear( heightInput ); // onChange is called and sets the dimension to undefined rather than // the empty string. expect( mockOnChange ).toHaveBeenCalledTimes( 1 ); - expect( mockOnChange ).toHaveBeenCalledWith( { + expect( mockOnChange ).toHaveBeenLastCalledWith( { height: undefined, } ); // Height is updated to empty value and does not reset to default. - expect( heightInput.value ).toBe( '' ); - expect( widthInput.value ).toBe( '100' ); + expect( heightInput ).toHaveValue( null ); + expect( widthInput ).toHaveValue( 100 ); } ); - it( 'updates width and calls onChange for empty value', () => { + it( 'updates width and calls onChange for empty value', async () => { + const user = userEvent.setup(); + render( { /> ); - const heightInput = getHeightInput(); - const widthInput = getWidthInput(); + const heightInput = screen.getByRole( 'spinbutton', { + name: 'Height', + } ); + const widthInput = screen.getByRole( 'spinbutton', { + name: 'Width', + } ); - expect( heightInput.value ).toBe( '100' ); - expect( widthInput.value ).toBe( '100' ); + expect( heightInput ).toHaveValue( 100 ); + expect( widthInput ).toHaveValue( 100 ); - fireEvent.change( widthInput, { target: { value: '' } } ); + await user.clear( widthInput ); // onChange is called and sets the dimension to undefined rather than // the empty string. expect( mockOnChange ).toHaveBeenCalledTimes( 1 ); - expect( mockOnChange ).toHaveBeenCalledWith( { + expect( mockOnChange ).toHaveBeenLastCalledWith( { width: undefined, } ); // Width is updated to empty value and does not reset to default. - expect( heightInput.value ).toBe( '100' ); - expect( widthInput.value ).toBe( '' ); + expect( heightInput ).toHaveValue( 100 ); + expect( widthInput ).toHaveValue( null ); } ); } ); describe( 'reset button', () => { - it( 'resets both height and width to default values', () => { + it( 'resets both height and width to default values', async () => { + const user = userEvent.setup(); + render( { /> ); - const heightInput = getHeightInput(); - const widthInput = getWidthInput(); + const heightInput = screen.getByRole( 'spinbutton', { + name: 'Height', + } ); + const widthInput = screen.getByRole( 'spinbutton', { + name: 'Width', + } ); // The initial dimension values display first. - expect( heightInput.value ).toBe( '300' ); - expect( widthInput.value ).toBe( '400' ); + expect( heightInput ).toHaveValue( 300 ); + expect( widthInput ).toHaveValue( 400 ); - fireEvent.click( screen.getByText( 'Reset' ) ); + await user.click( screen.getByRole( 'button', { name: 'Reset' } ) ); // Both attributes are set to undefined to clear custom values. - expect( mockOnChange ).toHaveBeenCalledWith( { + expect( mockOnChange ).toHaveBeenLastCalledWith( { height: undefined, width: undefined, } ); // The inputs display the default values once more. - expect( heightInput.value ).toBe( '100' ); - expect( widthInput.value ).toBe( '200' ); + expect( heightInput ).toHaveValue( 100 ); + expect( widthInput ).toHaveValue( 200 ); } ); } ); describe( 'image size percentage presets', () => { - it( 'updates height and width attributes on selection', () => { + it( 'updates height and width attributes on selection', async () => { + const user = userEvent.setup(); + render( { /> ); - fireEvent.click( screen.getByText( '50%' ) ); + const button = screen.getByRole( 'button', { + name: '50%', + pressed: false, + } ); + + await user.click( button ); - expect( screen.getByText( '50%' ) ).toHaveClass( 'is-pressed' ); + expect( button ).toHaveClass( 'is-pressed' ); // Both attributes are set to the rounded scaled value. - expect( mockOnChange ).toHaveBeenCalledWith( { + expect( mockOnChange ).toHaveBeenLastCalledWith( { height: 50, width: 101, } ); } ); - it( 'updates height and width inputs on selection', () => { + it( 'updates height and width inputs on selection', async () => { + const user = userEvent.setup(); + render( { /> ); - fireEvent.click( screen.getByText( '50%' ) ); + const button = screen.getByRole( 'button', { + name: '50%', + pressed: false, + } ); + + await user.click( button ); // Both attributes are set to the rounded scaled value. - expect( getHeightInput().value ).toBe( '50' ); - expect( getWidthInput().value ).toBe( '101' ); + expect( + screen.getByRole( 'spinbutton', { name: 'Height' } ) + ).toHaveValue( 50 ); + expect( + screen.getByRole( 'spinbutton', { name: 'Width' } ) + ).toHaveValue( 101 ); } ); } ); @@ -268,12 +333,14 @@ describe( 'ImageSizeControl', () => { /> ); - expect( screen.getByLabelText( 'Image size' ).value ).toBe( - 'medium' - ); + expect( + screen.getByRole( 'combobox', { name: 'Image size' } ) + ).toHaveValue( 'medium' ); } ); - it( 'calls onChangeImage with selected slug on selection', () => { + it( 'calls onChangeImage with selected slug on selection', async () => { + const user = userEvent.setup(); + render( { /> ); - fireEvent.change( screen.getByLabelText( 'Image size' ), { - target: { value: 'thumbnail' }, - } ); + await user.selectOptions( + screen.getByRole( 'combobox', { name: 'Image size' } ), + 'thumbnail' + ); // onChangeImage is called with the slug and the event. - expect( mockOnChangeImage ).toHaveBeenCalledWith( + expect( mockOnChangeImage ).toHaveBeenLastCalledWith( 'thumbnail', expect.any( Object ) ); diff --git a/packages/block-editor/src/components/inner-blocks/index.js b/packages/block-editor/src/components/inner-blocks/index.js index 9d0b3f34bc7729..0512569fb3f42d 100644 --- a/packages/block-editor/src/components/inner-blocks/index.js +++ b/packages/block-editor/src/components/inner-blocks/index.js @@ -87,8 +87,10 @@ function UncontrolledInnerBlocks( props ) { const blockType = getBlockType( block.name ); - if ( ! blockType || ! blockType.providesContext ) { - return {}; + if ( + Object.keys( blockType?.providesContext ?? {} ).length === 0 + ) { + return { name: block.name }; } return { diff --git a/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js b/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js index d2bbaa909d0a29..52c7f9ba23f83e 100644 --- a/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js +++ b/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js @@ -39,6 +39,7 @@ function useInsertionPoint( { isAppender, onSelect, shouldFocusBlock = true, + selectBlockOnInsert = true, } ) { const { getSelectedBlock } = useSelect( blockEditorStore ); const { destinationRootClientId, destinationIndex } = useSelect( @@ -108,7 +109,7 @@ function useInsertionPoint( { blocks, destinationIndex, destinationRootClientId, - true, + selectBlockOnInsert, shouldFocusBlock || shouldForceFocusBlock ? 0 : null, meta ); @@ -122,7 +123,7 @@ function useInsertionPoint( { speak( message ); if ( onSelect ) { - onSelect(); + onSelect( blocks ); } }, [ diff --git a/packages/block-editor/src/components/inserter/index.js b/packages/block-editor/src/components/inserter/index.js index 59c04e24d2bbb4..258faff2b826ab 100644 --- a/packages/block-editor/src/components/inserter/index.js +++ b/packages/block-editor/src/components/inserter/index.js @@ -143,18 +143,31 @@ class Inserter extends Component { // Feel free to make them stable after a few releases. __experimentalIsQuick: isQuick, prioritizePatterns, + onSelectOrClose, + selectBlockOnInsert, } = this.props; if ( isQuick ) { return ( { + onSelect={ ( blocks ) => { + const firstBlock = + Array.isArray( blocks ) && blocks?.length + ? blocks[ 0 ] + : blocks; + if ( + onSelectOrClose && + typeof onSelectOrClose === 'function' + ) { + onSelectOrClose( firstBlock ); + } onClose(); } } rootClientId={ rootClientId } clientId={ clientId } isAppender={ isAppender } prioritizePatterns={ prioritizePatterns } + selectBlockOnInsert={ selectBlockOnInsert } /> ); } @@ -380,7 +393,7 @@ export default compose( [ if ( onSelectOrClose ) { onSelectOrClose( { - insertedBlockId: blockToInsert?.clientId, + clientId: blockToInsert?.clientId, } ); } diff --git a/packages/block-editor/src/components/inserter/media-tab/hooks.js b/packages/block-editor/src/components/inserter/media-tab/hooks.js index f537dce68608ad..e3b68510d77d2e 100644 --- a/packages/block-editor/src/components/inserter/media-tab/hooks.js +++ b/packages/block-editor/src/components/inserter/media-tab/hooks.js @@ -66,14 +66,15 @@ export function useMediaCategories( rootClientId ) { per_page: 1, _fields: [ 'id' ], }; - const [ image, video, audio ] = await Promise.all( [ + const [ image, video, audio ] = await Promise.allSettled( [ fetchMedia( { ...query, media_type: 'image' } ), fetchMedia( { ...query, media_type: 'video' } ), fetchMedia( { ...query, media_type: 'audio' } ), ] ); - const showImage = canInsertImage && !! image.length; - const showVideo = canInsertVideo && !! video.length; - const showAudio = canInsertAudio && !! audio.length; + // The `value` property is only present if the promise's status is "fulfilled". + const showImage = canInsertImage && !! image.value?.length; + const showVideo = canInsertVideo && !! video.value?.length; + const showAudio = canInsertAudio && !! audio.value?.length; setCategories( MEDIA_CATEGORIES.filter( ( { mediaType } ) => diff --git a/packages/block-editor/src/components/inserter/quick-inserter.js b/packages/block-editor/src/components/inserter/quick-inserter.js index 39a5ac5c75b849..540b51a4757e0d 100644 --- a/packages/block-editor/src/components/inserter/quick-inserter.js +++ b/packages/block-editor/src/components/inserter/quick-inserter.js @@ -31,6 +31,7 @@ export default function QuickInserter( { clientId, isAppender, prioritizePatterns, + selectBlockOnInsert, } ) { const [ filterValue, setFilterValue ] = useState( '' ); const [ destinationRootClientId, onInsertBlocks ] = useInsertionPoint( { @@ -38,6 +39,7 @@ export default function QuickInserter( { rootClientId, clientId, isAppender, + selectBlockOnInsert, } ); const [ blockTypes ] = useBlockTypesState( destinationRootClientId, @@ -121,6 +123,7 @@ export default function QuickInserter( { maxBlockTypes={ SHOWN_BLOCK_TYPES } isDraggable={ false } prioritizePatterns={ prioritizePatterns } + selectBlockOnInsert={ selectBlockOnInsert } />
diff --git a/packages/block-editor/src/components/inserter/search-items.js b/packages/block-editor/src/components/inserter/search-items.js index 5451f19937b01d..35346fca09f32d 100644 --- a/packages/block-editor/src/components/inserter/search-items.js +++ b/packages/block-editor/src/components/inserter/search-items.js @@ -2,7 +2,6 @@ * External dependencies */ import removeAccents from 'remove-accents'; -import { find } from 'lodash'; import { noCase } from 'change-case'; // Default search helpers. @@ -88,7 +87,7 @@ export const searchBlockItems = ( const config = { getCategory: ( item ) => - find( categories, { slug: item.category } )?.title, + categories.find( ( { slug } ) => slug === item.category )?.title, getCollection: ( item ) => collections[ item.name.split( '/' )[ 0 ] ]?.title, }; diff --git a/packages/block-editor/src/components/inserter/search-results.js b/packages/block-editor/src/components/inserter/search-results.js index dfd7a3d73312d5..f55e49bd1cc80f 100644 --- a/packages/block-editor/src/components/inserter/search-results.js +++ b/packages/block-editor/src/components/inserter/search-results.js @@ -50,6 +50,7 @@ function InserterSearchResults( { isDraggable = true, shouldFocusBlock = true, prioritizePatterns, + selectBlockOnInsert, } ) { const debouncedSpeak = useDebounce( speak, 500 ); @@ -60,6 +61,7 @@ function InserterSearchResults( { isAppender, insertionIndex: __experimentalInsertionIndex, shouldFocusBlock, + selectBlockOnInsert, } ); const [ blockTypes, diff --git a/packages/block-editor/src/components/link-control/test/index.js b/packages/block-editor/src/components/link-control/test/index.js index 4c0a95351a666c..86eae7289f4f61 100644 --- a/packages/block-editor/src/components/link-control/test/index.js +++ b/packages/block-editor/src/components/link-control/test/index.js @@ -376,9 +376,15 @@ describe( 'Searching for a link', () => { const searchResults = await screen.findByRole( 'listbox', { name: /Search results for.*/, } ); - const searchResultTextHighlightElements = Array.from( - searchResults.querySelectorAll( 'button[role="option"] mark' ) - ); + + const searchResultTextHighlightElements = within( searchResults ) + .getAllByRole( 'option' ) + // TODO: Change to `getByRole( 'mark' )` when officially supported by + // WAI-ARIA 1.3 - see https://w3c.github.io/aria/#mark + // eslint-disable-next-line testing-library/no-node-access + .map( ( searchResult ) => searchResult.querySelector( 'mark' ) ) + .flat() + .filter( Boolean ); // Given we're mocking out the results we should always have 4 mark elements. expect( searchResultTextHighlightElements ).toHaveLength( 4 ); @@ -1092,6 +1098,7 @@ describe( 'Creating Entities (eg: Posts, Pages)', () => { // Check human readable error notice is perceivable. expect( errorNotice ).toBeVisible(); + // eslint-disable-next-line testing-library/no-node-access expect( errorNotice.parentElement ).toHaveClass( 'block-editor-link-control__search-error' ); @@ -1324,7 +1331,8 @@ describe( 'Selecting links', () => { } ); // Make sure focus is retained after submission. - expect( container ).toContainElement( document.activeElement ); + // eslint-disable-next-line testing-library/no-node-access + expect( container.firstChild ).toHaveFocus(); expect( currentLink ).toBeVisible(); expect( @@ -1594,11 +1602,13 @@ describe( 'Rich link previews', () => { await waitFor( () => expect( linkPreview ).toHaveClass( 'is-rich' ) ); // Todo: refactor to use user-facing queries. + // eslint-disable-next-line testing-library/no-node-access const hasRichImagePreview = linkPreview.querySelector( '.block-editor-link-control__search-item-image' ); // Todo: refactor to use user-facing queries. + // eslint-disable-next-line testing-library/no-node-access const hasRichDescriptionPreview = linkPreview.querySelector( '.block-editor-link-control__search-item-description' ); @@ -1646,11 +1656,14 @@ describe( 'Rich link previews', () => { await waitFor( () => expect( linkPreview ).toHaveClass( 'is-rich' ) ); + // eslint-disable-next-line testing-library/no-node-access const iconPreview = linkPreview.querySelector( `.block-editor-link-control__search-item-icon` ); + // eslint-disable-next-line testing-library/no-node-access const fallBackIcon = iconPreview.querySelector( 'svg' ); + // eslint-disable-next-line testing-library/no-node-access const richIcon = iconPreview.querySelector( 'img' ); expect( fallBackIcon ).toBeVisible(); @@ -1680,6 +1693,7 @@ describe( 'Rich link previews', () => { expect( linkPreview ).toHaveClass( 'is-rich' ) ); + // eslint-disable-next-line testing-library/no-node-access const missingDataItem = linkPreview.querySelector( `.block-editor-link-control__search-item-${ dataItem }` ); diff --git a/packages/block-editor/src/components/list-view/block-select-button.js b/packages/block-editor/src/components/list-view/block-select-button.js index 9477eb2cda40c0..39baf78d5a7a88 100644 --- a/packages/block-editor/src/components/list-view/block-select-button.js +++ b/packages/block-editor/src/components/list-view/block-select-button.js @@ -12,7 +12,7 @@ import { __experimentalTruncate as Truncate, } from '@wordpress/components'; import { forwardRef } from '@wordpress/element'; -import { Icon, lock } from '@wordpress/icons'; +import { Icon, lockSmall as lock } from '@wordpress/icons'; import { SPACE, ENTER } from '@wordpress/keycodes'; /** diff --git a/packages/block-editor/src/components/list-view/style.scss b/packages/block-editor/src/components/list-view/style.scss index 0c271352b91552..61c6f80dab608e 100644 --- a/packages/block-editor/src/components/list-view/style.scss +++ b/packages/block-editor/src/components/list-view/style.scss @@ -176,9 +176,8 @@ } .block-editor-block-icon { - align-self: flex-start; margin-right: $grid-unit-10; - width: $icon-size; + flex: 0 0 $icon-size; } .block-editor-list-view-block__menu-cell, @@ -319,11 +318,6 @@ .block-editor-list-view-block-select-button__lock { line-height: 0; - width: 24px; - min-width: 24px; - margin-left: auto; - padding: 0; - vertical-align: middle; } } diff --git a/packages/block-editor/src/components/media-replace-flow/test/index.js b/packages/block-editor/src/components/media-replace-flow/test/index.js index 94d93a65ef8738..8b1fc612280137 100644 --- a/packages/block-editor/src/components/media-replace-flow/test/index.js +++ b/packages/block-editor/src/components/media-replace-flow/test/index.js @@ -14,6 +14,8 @@ import { useState } from '@wordpress/element'; */ import MediaReplaceFlow from '../'; +jest.useFakeTimers(); + const noop = () => {}; function TestWrapper() { diff --git a/packages/block-editor/src/components/off-canvas-editor/appender.js b/packages/block-editor/src/components/off-canvas-editor/appender.js index 646700143cf511..5571db2816382d 100644 --- a/packages/block-editor/src/components/off-canvas-editor/appender.js +++ b/packages/block-editor/src/components/off-canvas-editor/appender.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useSelect, useDispatch } from '@wordpress/data'; +import { useSelect } from '@wordpress/data'; import { forwardRef, useState } from '@wordpress/element'; /** * Internal dependencies @@ -10,9 +10,15 @@ import { store as blockEditorStore } from '../../store'; import Inserter from '../inserter'; import { LinkUI } from './link-ui'; import { updateAttributes } from './update-attributes'; +import { useInsertedBlock } from './use-inserted-block'; + +const BLOCKS_WITH_LINK_UI_SUPPORT = [ + 'core/navigation-link', + 'core/navigation-submenu', +]; export const Appender = forwardRef( ( props, ref ) => { - const [ insertedBlock, setInsertedBlock ] = useState(); + const [ insertedBlockClientId, setInsertedBlockClientId ] = useState(); const { hideInserter, clientId } = useSelect( ( select ) => { const { @@ -31,40 +37,39 @@ export const Appender = forwardRef( ( props, ref ) => { }; }, [] ); - const { insertedBlockAttributes } = useSelect( - ( select ) => { - const { getBlockAttributes } = select( blockEditorStore ); - - return { - insertedBlockAttributes: getBlockAttributes( insertedBlock ), - }; - }, - [ insertedBlock ] - ); + const { + insertedBlockAttributes, + insertedBlockName, + setInsertedBlockAttributes, + } = useInsertedBlock( insertedBlockClientId ); - const { updateBlockAttributes } = useDispatch( blockEditorStore ); + const maybeSetInsertedBlockOnInsertion = ( _insertedBlock ) => { + if ( ! _insertedBlock?.clientId ) { + return; + } - const setAttributes = - ( insertedBlockClientId ) => ( _updatedAttributes ) => { - updateBlockAttributes( insertedBlockClientId, _updatedAttributes ); - }; + setInsertedBlockClientId( _insertedBlock?.clientId ); + }; let maybeLinkUI; - if ( insertedBlock ) { + if ( + insertedBlockClientId && + BLOCKS_WITH_LINK_UI_SUPPORT?.includes( insertedBlockName ) + ) { maybeLinkUI = ( setInsertedBlock( null ) } + onClose={ () => setInsertedBlockClientId( null ) } hasCreateSuggestion={ false } onChange={ ( updatedValue ) => { updateAttributes( updatedValue, - setAttributes( insertedBlock ), + setInsertedBlockAttributes, insertedBlockAttributes ); - setInsertedBlock( null ); + setInsertedBlockClientId( null ); } } /> ); @@ -77,15 +82,15 @@ export const Appender = forwardRef( ( props, ref ) => { return (
{ maybeLinkUI } + { - setInsertedBlock( insertedBlockId ); - } } + onSelectOrClose={ maybeSetInsertedBlockOnInsertion } + __experimentalIsQuick { ...props } />
diff --git a/packages/block-editor/src/components/off-canvas-editor/block-select-button.js b/packages/block-editor/src/components/off-canvas-editor/block-select-button.js index 9477eb2cda40c0..543823ab1ada25 100644 --- a/packages/block-editor/src/components/off-canvas-editor/block-select-button.js +++ b/packages/block-editor/src/components/off-canvas-editor/block-select-button.js @@ -12,7 +12,7 @@ import { __experimentalTruncate as Truncate, } from '@wordpress/components'; import { forwardRef } from '@wordpress/element'; -import { Icon, lock } from '@wordpress/icons'; +import { Icon, lockSmall as lock } from '@wordpress/icons'; import { SPACE, ENTER } from '@wordpress/keycodes'; /** @@ -79,7 +79,11 @@ function ListViewBlockSelectButton( aria-hidden={ true } > - + select( blockEditorStore ).getBlockName( clientId ), + const block = useSelect( + ( select ) => select( blockEditorStore ).getBlock( clientId ), [ clientId ] ); + // If ListView has experimental features related to the Persistent List View, + // only focus the selected list item on mount; otherwise the list would always + // try to steal the focus from the editor canvas. + useEffect( () => { + if ( ! isTreeGridMounted && isSelected ) { + cellRef.current.focus(); + } + }, [] ); + + const onMouseEnter = useCallback( () => { + setIsHovered( true ); + toggleBlockHighlight( clientId, true ); + }, [ clientId, setIsHovered, toggleBlockHighlight ] ); + const onMouseLeave = useCallback( () => { + setIsHovered( false ); + toggleBlockHighlight( clientId, false ); + }, [ clientId, setIsHovered, toggleBlockHighlight ] ); + + const selectEditorBlock = useCallback( + ( event ) => { + selectBlock( event, clientId ); + event.preventDefault(); + }, + [ clientId, selectBlock ] + ); + + const updateSelection = useCallback( + ( newClientId ) => { + selectBlock( undefined, newClientId ); + }, + [ selectBlock ] + ); + + const { isTreeGridMounted, expand, collapse } = useListViewContext(); + + const toggleExpanded = useCallback( + ( event ) => { + // Prevent shift+click from opening link in a new window when toggling. + event.preventDefault(); + event.stopPropagation(); + if ( isExpanded === true ) { + collapse( clientId ); + } else if ( isExpanded === false ) { + expand( clientId ); + } + }, + [ clientId, expand, collapse, isExpanded ] + ); + + const instanceId = useInstanceId( ListViewBlock ); + + if ( ! block ) { + return null; + } + // When a block hides its toolbar it also hides the block settings menu, // since that menu is part of the toolbar in the editor canvas. // List View respects this by also hiding the block settings menu. - const showBlockActions = hasBlockSupport( - blockName, - '__experimentalToolbar', - true - ); - const instanceId = useInstanceId( ListViewBlock ); + const showBlockActions = + !! block && + hasBlockSupport( block.name, '__experimentalToolbar', true ); + const descriptionId = `list-view-block-select-button__${ instanceId }`; const blockPositionDescription = getBlockPositionDescription( position, @@ -143,9 +195,7 @@ function ListViewBlock( { ) : __( 'Edit' ); - const { isTreeGridMounted, expand, collapse } = useListViewContext(); - - const isEditable = block.name !== 'core/page-list-item'; + const isEditable = !! block && block.name !== 'core/page-list-item'; const hasSiblings = siblingBlockCount > 0; const hasRenderedMovers = showBlockMovers && hasSiblings; const moverCellClassName = classnames( @@ -163,53 +213,6 @@ function ListViewBlock( { { 'is-visible': isHovered || isFirstSelectedBlock } ); - // If ListView has experimental features related to the Persistent List View, - // only focus the selected list item on mount; otherwise the list would always - // try to steal the focus from the editor canvas. - useEffect( () => { - if ( ! isTreeGridMounted && isSelected ) { - cellRef.current.focus(); - } - }, [] ); - - const onMouseEnter = useCallback( () => { - setIsHovered( true ); - toggleBlockHighlight( clientId, true ); - }, [ clientId, setIsHovered, toggleBlockHighlight ] ); - const onMouseLeave = useCallback( () => { - setIsHovered( false ); - toggleBlockHighlight( clientId, false ); - }, [ clientId, setIsHovered, toggleBlockHighlight ] ); - - const selectEditorBlock = useCallback( - ( event ) => { - selectBlock( event, clientId ); - event.preventDefault(); - }, - [ clientId, selectBlock ] - ); - - const updateSelection = useCallback( - ( newClientId ) => { - selectBlock( undefined, newClientId ); - }, - [ selectBlock ] - ); - - const toggleExpanded = useCallback( - ( event ) => { - // Prevent shift+click from opening link in a new window when toggling. - event.preventDefault(); - event.stopPropagation(); - if ( isExpanded === true ) { - collapse( clientId ); - } else if ( isExpanded === false ) { - expand( clientId ); - } - }, - [ clientId, expand, collapse, isExpanded ] - ); - let colSpan; if ( hasRenderedMovers ) { colSpan = 2; @@ -369,24 +372,38 @@ function ListViewBlock( { const newLink = createBlock( 'core/navigation-link' ); - const newSubmenu = createBlock( - 'core/navigation-submenu', - attributes, - block.innerBlocks - ? [ - ...block.innerBlocks, - newLink, - ] - : [ newLink ] - ); - replaceBlock( - clientId, - newSubmenu - ); + if ( + block.name === + 'core/navigation-submenu' + ) { + const updateSelectionOnInsert = false; + insertBlock( + newLink, + block.innerBlocks.length, + clientId, + updateSelectionOnInsert + ); + } else { + // Convert to a submenu if the block currently isn't one. + const newSubmenu = createBlock( + 'core/navigation-submenu', + block.attributes, + block.innerBlocks + ? [ + ...block.innerBlocks, + newLink, + ] + : [ newLink ] + ); + replaceBlock( + clientId, + newSubmenu + ); + } onClose(); } } > - { __( 'Add a submenu item' ) } + { __( 'Add submenu item' ) } ) } diff --git a/packages/block-editor/src/components/off-canvas-editor/index.js b/packages/block-editor/src/components/off-canvas-editor/index.js index 7d38d7c83c3350..2642d6605baac1 100644 --- a/packages/block-editor/src/components/off-canvas-editor/index.js +++ b/packages/block-editor/src/components/off-canvas-editor/index.js @@ -231,6 +231,15 @@ function __ExperimentalOffCanvasEditor( ) } + { ! clientIdsTree.length && ( + +
+ { __( + 'Your menu is currently empty. Add your first menu item to get started.' + ) } +
+
+ ) } diff --git a/packages/block-editor/src/components/off-canvas-editor/style.scss b/packages/block-editor/src/components/off-canvas-editor/style.scss index 8fedf057b03333..e65646a318d3d6 100644 --- a/packages/block-editor/src/components/off-canvas-editor/style.scss +++ b/packages/block-editor/src/components/off-canvas-editor/style.scss @@ -1,7 +1,7 @@ .offcanvas-editor__appender .block-editor-inserter__toggle { background-color: #1e1e1e; color: #fff; - margin: $grid-unit-10 0 0 28px; + margin: $grid-unit-10 0 0 24px; border-radius: 2px; height: 24px; min-width: 24px; @@ -24,3 +24,7 @@ // sidebar width - tab panel padding max-width: $sidebar-width - (2 * $grid-unit-20); } + +.offcanvas-editor-list-view-is-empty { + margin-left: $grid-unit-20; +} diff --git a/packages/block-editor/src/components/off-canvas-editor/test/use-inserted-block.js b/packages/block-editor/src/components/off-canvas-editor/test/use-inserted-block.js new file mode 100644 index 00000000000000..f4e6746581e008 --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/test/use-inserted-block.js @@ -0,0 +1,108 @@ +/** + * Internal dependencies + */ +import { useInsertedBlock } from '../use-inserted-block'; + +/** + * WordPress dependencies + */ +import { useDispatch, useSelect } from '@wordpress/data'; + +/** + * External dependencies + */ +import { act, renderHook } from '@testing-library/react'; + +jest.mock( '@wordpress/data/src/components/use-select', () => { + // This allows us to tweak the returned value on each test. + const mock = jest.fn(); + return mock; +} ); + +jest.mock( '@wordpress/data/src/components/use-dispatch', () => ( { + useDispatch: jest.fn(), +} ) ); + +describe( 'useInsertedBlock', () => { + const mockUpdateBlockAttributes = jest.fn(); + + it( 'returns undefined values when called without a block clientId', () => { + useSelect.mockImplementation( () => ( { + insertedBlockAttributes: { + 'some-attribute': 'some-value', + }, + insertedBlockName: 'core/navigation-link', + } ) ); + + useDispatch.mockImplementation( () => ( { + updateBlockAttributes: mockUpdateBlockAttributes, + } ) ); + + const { result } = renderHook( () => useInsertedBlock() ); + + const { + insertedBlockName, + insertedBlockAttributes, + setInsertedBlockAttributes, + } = result.current; + + expect( insertedBlockName ).toBeUndefined(); + expect( insertedBlockAttributes ).toBeUndefined(); + expect( + setInsertedBlockAttributes( { 'some-attribute': 'new-value' } ) + ).toBeUndefined(); + } ); + + it( 'returns name and attributes when called with a block clientId', () => { + useSelect.mockImplementation( () => ( { + insertedBlockAttributes: { + 'some-attribute': 'some-value', + }, + insertedBlockName: 'core/navigation-link', + } ) ); + + useDispatch.mockImplementation( () => ( { + updateBlockAttributes: mockUpdateBlockAttributes, + } ) ); + + const { result } = renderHook( () => + useInsertedBlock( 'some-client-id-here' ) + ); + + const { insertedBlockName, insertedBlockAttributes } = result.current; + + expect( insertedBlockName ).toBe( 'core/navigation-link' ); + expect( insertedBlockAttributes ).toEqual( { + 'some-attribute': 'some-value', + } ); + } ); + + it( 'dispatches updateBlockAttributes on provided client ID with new attributes when setInsertedBlockAttributes is called', () => { + useSelect.mockImplementation( () => ( { + insertedBlockAttributes: { + 'some-attribute': 'some-value', + }, + insertedBlockName: 'core/navigation-link', + } ) ); + + useDispatch.mockImplementation( () => ( { + updateBlockAttributes: mockUpdateBlockAttributes, + } ) ); + + const clientId = '123456789'; + + const { result } = renderHook( () => useInsertedBlock( clientId ) ); + + const { setInsertedBlockAttributes } = result.current; + + act( () => { + setInsertedBlockAttributes( { + 'some-attribute': 'new-value', + } ); + } ); + + expect( mockUpdateBlockAttributes ).toHaveBeenCalledWith( clientId, { + 'some-attribute': 'new-value', + } ); + } ); +} ); diff --git a/packages/block-editor/src/components/off-canvas-editor/use-inserted-block.js b/packages/block-editor/src/components/off-canvas-editor/use-inserted-block.js new file mode 100644 index 00000000000000..0e5a25c980a1c3 --- /dev/null +++ b/packages/block-editor/src/components/off-canvas-editor/use-inserted-block.js @@ -0,0 +1,47 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; + +export const useInsertedBlock = ( insertedBlockClientId ) => { + const { insertedBlockAttributes, insertedBlockName } = useSelect( + ( select ) => { + const { getBlockName, getBlockAttributes } = + select( blockEditorStore ); + + return { + insertedBlockAttributes: getBlockAttributes( + insertedBlockClientId + ), + insertedBlockName: getBlockName( insertedBlockClientId ), + }; + }, + [ insertedBlockClientId ] + ); + + const { updateBlockAttributes } = useDispatch( blockEditorStore ); + + const setInsertedBlockAttributes = ( _updatedAttributes ) => { + if ( ! insertedBlockClientId ) return; + updateBlockAttributes( insertedBlockClientId, _updatedAttributes ); + }; + + if ( ! insertedBlockClientId ) { + return { + insertedBlockAttributes: undefined, + insertedBlockName: undefined, + setInsertedBlockAttributes, + }; + } + + return { + insertedBlockAttributes, + insertedBlockName, + setInsertedBlockAttributes, + }; +}; diff --git a/packages/block-editor/src/components/responsive-block-control/label.js b/packages/block-editor/src/components/responsive-block-control/label.js index b0bd0c180cb902..49be9ba5227ed1 100644 --- a/packages/block-editor/src/components/responsive-block-control/label.js +++ b/packages/block-editor/src/components/responsive-block-control/label.js @@ -4,7 +4,6 @@ import { useInstanceId } from '@wordpress/compose'; import { VisuallyHidden } from '@wordpress/components'; import { _x, sprintf } from '@wordpress/i18n'; -import { Fragment } from '@wordpress/element'; export default function ResponsiveBlockControlLabel( { property, @@ -24,13 +23,13 @@ export default function ResponsiveBlockControlLabel( { viewport.label ); return ( - + <> { viewport.label } { accessibleLabel } - + ); } diff --git a/packages/block-editor/src/components/responsive-block-control/test/index.js b/packages/block-editor/src/components/responsive-block-control/test/index.js index f0454dcc87bc76..f2d5a5a1a3b880 100644 --- a/packages/block-editor/src/components/responsive-block-control/test/index.js +++ b/packages/block-editor/src/components/responsive-block-control/test/index.js @@ -15,6 +15,8 @@ import { SelectControl } from '@wordpress/components'; */ import ResponsiveBlockControl from '../index'; +jest.useFakeTimers(); + const inputId = 'input-12345678'; const sizeOptions = [ @@ -38,13 +40,13 @@ const sizeOptions = [ const renderTestDefaultControlComponent = ( labelComponent, device ) => { return ( - + <>

{ device.label } is used here for testing purposes to ensure we have access to details about the device.

-
+ ); }; diff --git a/packages/block-editor/src/components/rich-text/file-paste-handler.js b/packages/block-editor/src/components/rich-text/file-paste-handler.js deleted file mode 100644 index 2aae5984389e68..00000000000000 --- a/packages/block-editor/src/components/rich-text/file-paste-handler.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * WordPress dependencies - */ -import { createBlobURL } from '@wordpress/blob'; - -export function filePasteHandler( files ) { - return files - .filter( ( { type } ) => - /^image\/(?:jpe?g|png|gif|webp)$/.test( type ) - ) - .map( ( file ) => `` ) - .join( '' ); -} diff --git a/packages/block-editor/src/components/rich-text/format-edit.js b/packages/block-editor/src/components/rich-text/format-edit.js index 3c6861c341cd4b..3ab3f47d226aec 100644 --- a/packages/block-editor/src/components/rich-text/format-edit.js +++ b/packages/block-editor/src/components/rich-text/format-edit.js @@ -6,10 +6,6 @@ import { getActiveObject, isCollapsed, } from '@wordpress/rich-text'; -/** - * External dependencies - */ -import { find } from 'lodash'; export default function FormatEdit( { formatTypes, @@ -40,13 +36,13 @@ export default function FormatEdit( { if ( name === 'core/link' && ! isCollapsed( value ) ) { const formats = value.formats; - const linkFormatAtStart = find( formats[ value.start ], { - type: 'core/link', - } ); + const linkFormatAtStart = formats[ value.start ]?.find( + ( { type } ) => type === 'core/link' + ); - const linkFormatAtEnd = find( formats[ value.end - 1 ], { - type: 'core/link', - } ); + const linkFormatAtEnd = formats[ value.end - 1 ]?.find( + ( { type } ) => type === 'core/link' + ); if ( ! linkFormatAtStart || diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 726f8cddfff7b7..8219272ad3043a 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -368,6 +368,7 @@ function RichTextWrapper( { children && children( { value, onChange, onFocus } ) } + { + const transformation = findTransform( + fromTransforms, + ( transform ) => + transform.type === 'files' && + transform.isMatch( [ file ] ) + ); + if ( transformation ) { + accumulator.push( + transformation.transform( [ file ] ) + ); + } + return accumulator; + }, [] ) + .flat(); + if ( ! blocks.length ) { + return; + } if ( onReplace && isEmpty( value ) ) { - onReplace( content ); + onReplace( blocks ); } else { splitValue( { value, - pastedBlocks: content, + pastedBlocks: blocks, onReplace, onSplit, onSplitMiddle, diff --git a/packages/block-editor/src/components/spacing-sizes-control/index.js b/packages/block-editor/src/components/spacing-sizes-control/index.js index fb4ce2176b759d..4ec1285db52bb9 100644 --- a/packages/block-editor/src/components/spacing-sizes-control/index.js +++ b/packages/block-editor/src/components/spacing-sizes-control/index.js @@ -78,7 +78,6 @@ export default function SpacingSizesControl( { return (
{ it( 'should render a `Insert link` button and not be pressed when `url` is not provided', () => { render( ); diff --git a/packages/block-editor/src/components/url-popover/image-url-input-ui.js b/packages/block-editor/src/components/url-popover/image-url-input-ui.js index ea3b6fbbe0a199..471fbf2044d511 100644 --- a/packages/block-editor/src/components/url-popover/image-url-input-ui.js +++ b/packages/block-editor/src/components/url-popover/image-url-input-ui.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { find, map } from 'lodash'; +import { map } from 'lodash'; /** * WordPress dependencies @@ -189,7 +189,7 @@ const ImageURLInputUI = ( { linkDestinationInput = LINK_DESTINATION_NONE; } else { linkDestinationInput = ( - find( linkDestinations, ( destination ) => { + linkDestinations.find( ( destination ) => { return destination.url === value; } ) || { linkDestination: LINK_DESTINATION_CUSTOM } ).linkDestination; @@ -236,8 +236,9 @@ const ImageURLInputUI = ( { const linkEditorValue = urlInput !== null ? urlInput : url; const urlLabel = ( - find( getLinkDestinations(), [ 'linkDestination', linkDestination ] ) || - {} + getLinkDestinations().find( + ( destination ) => destination.linkDestination === linkDestination + ) || {} ).title; return ( diff --git a/packages/block-editor/src/components/url-popover/test/index.js b/packages/block-editor/src/components/url-popover/test/index.js index 11eead4ea3fde9..859d21fd0305b5 100644 --- a/packages/block-editor/src/components/url-popover/test/index.js +++ b/packages/block-editor/src/components/url-popover/test/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { act, render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; /** @@ -11,6 +11,16 @@ import URLPopover from '../'; jest.useRealTimers(); +/** + * Returns the first found popover element up the DOM tree. + * + * @param {HTMLElement} element Element to start with. + * @return {HTMLElement|null} Popover element, or `null` if not found. + */ +function getWrappingPopoverElement( element ) { + return element.closest( '.components-popover' ); +} + describe( 'URLPopover', () => { it( 'matches the snapshot in its default state', async () => { const { container } = render( @@ -22,8 +32,11 @@ describe( 'URLPopover', () => { ); - // wait for `Popover` effects to finish - await act( () => Promise.resolve() ); + await waitFor( () => + expect( + getWrappingPopoverElement( screen.getByText( 'Editor' ) ) + ).toBePositionedPopover() + ); expect( container ).toMatchSnapshot(); } ); @@ -53,8 +66,11 @@ describe( 'URLPopover', () => { ); - // wait for `Popover` effects to finish - await act( () => Promise.resolve() ); + await waitFor( () => + expect( + getWrappingPopoverElement( screen.getByText( 'Editor' ) ) + ).toBePositionedPopover() + ); expect( container ).toMatchSnapshot(); } ); diff --git a/packages/block-editor/src/components/warning/test/index.js b/packages/block-editor/src/components/warning/test/index.js index 88c9006b8ace9d..0b9b5f353baafe 100644 --- a/packages/block-editor/src/components/warning/test/index.js +++ b/packages/block-editor/src/components/warning/test/index.js @@ -9,6 +9,8 @@ import userEvent from '@testing-library/user-event'; */ import Warning from '../index'; +jest.useFakeTimers(); + describe( 'Warning', () => { it( 'should match snapshot', () => { const { container } = render( error ); diff --git a/packages/block-editor/src/components/writing-flow/index.js b/packages/block-editor/src/components/writing-flow/index.js index b28f065c559c5f..e3100dc3464c65 100644 --- a/packages/block-editor/src/components/writing-flow/index.js +++ b/packages/block-editor/src/components/writing-flow/index.js @@ -44,7 +44,7 @@ export function useWritingFlow() { useArrowNav(), useRefEffect( ( node ) => { - node.tabIndex = -1; + node.tabIndex = 0; node.contentEditable = hasMultiSelection; if ( ! hasMultiSelection ) { diff --git a/packages/block-editor/src/hooks/border.js b/packages/block-editor/src/hooks/border.js index 11ee3e95f4d663..74e79ecec80488 100644 --- a/packages/block-editor/src/hooks/border.js +++ b/packages/block-editor/src/hooks/border.js @@ -274,7 +274,6 @@ export function BorderPanel( props ) { popoverPlacement="left-start" size="__unstable-large" value={ hydratedBorder } - __experimentalHasMultipleOrigins={ true } __experimentalIsRenderedInSidebar={ true } /> diff --git a/packages/block-editor/src/hooks/color-panel.js b/packages/block-editor/src/hooks/color-panel.js index d8b804c300eb04..6bf9ff036280d2 100644 --- a/packages/block-editor/src/hooks/color-panel.js +++ b/packages/block-editor/src/hooks/color-panel.js @@ -81,7 +81,6 @@ export default function ColorPanel( { panelId={ clientId } settings={ settings } __experimentalIsItemGroup={ false } - __experimentalHasMultipleOrigins __experimentalIsRenderedInSidebar { ...colorGradientSettings } /> diff --git a/packages/block-editor/src/hooks/color.js b/packages/block-editor/src/hooks/color.js index 593eea881e3107..fee6f39daa8b20 100644 --- a/packages/block-editor/src/hooks/color.js +++ b/packages/block-editor/src/hooks/color.js @@ -522,8 +522,6 @@ export function ColorEdit( props ) { allSolids, style?.elements?.link?.color?.text ), - clearable: - !! style?.elements?.link?.color?.text, isShownByDefault: defaultColorControls?.link, resetAllFilter: resetAllLinkFilter, }, diff --git a/packages/block-editor/src/hooks/font-family.js b/packages/block-editor/src/hooks/font-family.js index fe8693f64421bf..56335ced887074 100644 --- a/packages/block-editor/src/hooks/font-family.js +++ b/packages/block-editor/src/hooks/font-family.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { find, kebabCase } from 'lodash'; +import { kebabCase } from 'lodash'; /** * WordPress dependencies @@ -111,14 +111,12 @@ export function FontFamilyEdit( { } ) { const fontFamilies = useSetting( 'typography.fontFamilies' ); - const value = find( - fontFamilies, + const value = fontFamilies?.find( ( { slug } ) => fontFamily === slug )?.fontFamily; function onChange( newValue ) { - const predefinedFontFamily = find( - fontFamilies, + const predefinedFontFamily = fontFamilies?.find( ( { fontFamily: f } ) => f === newValue ); setAttributes( { diff --git a/packages/block-editor/src/hooks/font-size.js b/packages/block-editor/src/hooks/font-size.js index 6cb950afc45641..0c7a71fd23d683 100644 --- a/packages/block-editor/src/hooks/font-size.js +++ b/packages/block-editor/src/hooks/font-size.js @@ -324,13 +324,22 @@ function addEditPropsForFluidCustomFontSizes( blockType ) { // BlockListContext.Provider. If we set fontSize using editor. // BlockListBlock instead of using getEditWrapperProps then the value is // clobbered when the core/style/addEditProps filter runs. - const isFluidTypographyEnabled = - !! select( blockEditorStore ).getSettings().__experimentalFeatures + const fluidTypographyConfig = + select( blockEditorStore ).getSettings().__experimentalFeatures ?.typography?.fluid; + const fluidTypographySettings = + typeof fluidTypographyConfig === 'object' + ? fluidTypographyConfig + : {}; + const newFontSize = - fontSize && isFluidTypographyEnabled - ? getComputedFluidTypographyValue( { fontSize } ) + fontSize && !! fluidTypographyConfig + ? getComputedFluidTypographyValue( { + fontSize, + minimumFontSizeLimit: + fluidTypographySettings?.minFontSize, + } ) : null; if ( newFontSize === null ) { diff --git a/packages/block-editor/src/hooks/test/use-typography-props.js b/packages/block-editor/src/hooks/test/use-typography-props.js index 00557881467ca8..12336eb2c44afd 100644 --- a/packages/block-editor/src/hooks/test/use-typography-props.js +++ b/packages/block-editor/src/hooks/test/use-typography-props.js @@ -47,4 +47,30 @@ describe( 'getTypographyClassesAndStyles', () => { }, } ); } ); + + it( 'should return configured fluid font size styles', () => { + const attributes = { + fontFamily: 'tofu', + style: { + typography: { + textDecoration: 'underline', + fontSize: '2rem', + textTransform: 'uppercase', + }, + }, + }; + expect( + getTypographyClassesAndStyles( attributes, { + minFontSize: '1rem', + } ) + ).toEqual( { + className: 'has-tofu-font-family', + style: { + textDecoration: 'underline', + fontSize: + 'clamp(1.5rem, 1.5rem + ((1vw - 0.48rem) * 0.962), 2rem)', + textTransform: 'uppercase', + }, + } ); + } ); } ); diff --git a/packages/block-editor/src/hooks/use-typography-props.js b/packages/block-editor/src/hooks/use-typography-props.js index d70ae08aafc593..da5869ad9aec07 100644 --- a/packages/block-editor/src/hooks/use-typography-props.js +++ b/packages/block-editor/src/hooks/use-typography-props.js @@ -19,23 +19,31 @@ import { getComputedFluidTypographyValue } from '../components/font-sizes/fluid- * Provides the CSS class names and inline styles for a block's typography support * attributes. * - * @param {Object} attributes Block attributes. - * @param {boolean} isFluidFontSizeActive Whether the function should try to convert font sizes to fluid values. + * @param {Object} attributes Block attributes. + * @param {Object|boolean} fluidTypographySettings If boolean, whether the function should try to convert font sizes to fluid values, + * otherwise an object containing theme fluid typography settings. * * @return {Object} Typography block support derived CSS classes & styles. */ export function getTypographyClassesAndStyles( attributes, - isFluidFontSizeActive + fluidTypographySettings ) { let typographyStyles = attributes?.style?.typography || {}; - if ( isFluidFontSizeActive ) { + if ( + !! fluidTypographySettings && + ( true === fluidTypographySettings || + Object.keys( fluidTypographySettings ).length !== 0 ) + ) { + const newFontSize = + getComputedFluidTypographyValue( { + fontSize: attributes?.style?.typography?.fontSize, + minimumFontSizeLimit: fluidTypographySettings?.minFontSize, + } ) || attributes?.style?.typography?.fontSize; typographyStyles = { ...typographyStyles, - fontSize: getComputedFluidTypographyValue( { - fontSize: attributes?.style?.typography?.fontSize, - } ), + fontSize: newFontSize, }; } diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 9c99126f54ba14..a19983ce58ff46 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -2,7 +2,6 @@ * External dependencies */ import fastDeepEqual from 'fast-deep-equal/es6'; -import { omit, isEmpty } from 'lodash'; /** * WordPress dependencies @@ -113,7 +112,10 @@ function getFlattenedClientIds( blocks ) { * @return {Array} Flattened block attributes object. */ function getFlattenedBlocksWithoutAttributes( blocks ) { - return flattenBlocks( blocks, ( block ) => omit( block, 'attributes' ) ); + return flattenBlocks( blocks, ( block ) => { + const { attributes, ...restBlock } = block; + return restBlock; + } ); } /** @@ -782,8 +784,8 @@ export const blocks = pipe( } // Do nothing if only attributes change. - const changes = omit( action.updates, 'attributes' ); - if ( isEmpty( changes ) ) { + const { attributes, ...changes } = action.updates; + if ( Object.values( changes ).length === 0 ) { return state; } @@ -1641,13 +1643,18 @@ export const blockListSettings = ( state = {}, action ) => { // should correct the state. case 'REPLACE_BLOCKS': case 'REMOVE_BLOCKS': { - return omit( state, action.clientIds ); + return Object.fromEntries( + Object.entries( state ).filter( + ( [ id ] ) => ! action.clientIds.includes( id ) + ) + ); } case 'UPDATE_BLOCK_LIST_SETTINGS': { const { clientId } = action; if ( ! action.settings ) { if ( state.hasOwnProperty( clientId ) ) { - return omit( state, clientId ); + const { [ clientId ]: removedBlock, ...restBlocks } = state; + return restBlocks; } return state; diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 3c048a7d58a29c..f096767bf6178c 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { map, find } from 'lodash'; +import { map } from 'lodash'; import createSelector from 'rememo'; /** @@ -2469,8 +2469,7 @@ export const __experimentalGetBlockListSettingsForBlocks = createSelector( */ export const __experimentalGetReusableBlockTitle = createSelector( ( state, ref ) => { - const reusableBlock = find( - getReusableBlocks( state ), + const reusableBlock = getReusableBlocks( state ).find( ( block ) => block.id === ref ); if ( ! reusableBlock ) { diff --git a/packages/block-editor/src/utils/pasting.js b/packages/block-editor/src/utils/pasting.js index 366b79a3294229..e962e11050a1d9 100644 --- a/packages/block-editor/src/utils/pasting.js +++ b/packages/block-editor/src/utils/pasting.js @@ -1,7 +1,6 @@ /** * WordPress dependencies */ -import { createBlobURL } from '@wordpress/blob'; import { getFilesFromDataTransfer } from '@wordpress/dom'; export function getPasteEventData( { clipboardData } ) { @@ -25,21 +24,16 @@ export function getPasteEventData( { clipboardData } ) { } } - const files = getFilesFromDataTransfer( clipboardData ).filter( - ( { type } ) => /^image\/(?:jpe?g|png|gif|webp)$/.test( type ) - ); + const files = getFilesFromDataTransfer( clipboardData ); if ( files.length && ! shouldDismissPastedFiles( files, html, plainText ) ) { - html = files - .map( ( file ) => `` ) - .join( '' ); - plainText = ''; + return { files }; } - return { html, plainText }; + return { html, plainText, files: [] }; } /** diff --git a/packages/block-library/src/categories/edit.js b/packages/block-library/src/categories/edit.js index db014bac0fd150..84b210d7fadac0 100644 --- a/packages/block-library/src/categories/edit.js +++ b/packages/block-library/src/categories/edit.js @@ -63,7 +63,7 @@ export default function CategoriesEdit( { const parentId = showHierarchy ? 0 : null; const categoriesList = getCategoriesList( parentId ); return categoriesList.map( ( category ) => - renderCategoryListItem( category, 0 ) + renderCategoryListItem( category ) ); }; diff --git a/packages/block-library/src/comments/index.php b/packages/block-library/src/comments/index.php index 69044c081c74e0..a5e51cfbf4e793 100644 --- a/packages/block-library/src/comments/index.php +++ b/packages/block-library/src/comments/index.php @@ -202,7 +202,6 @@ function register_legacy_post_comments_block() { 'wp-block-buttons', 'wp-block-button', ), - 'editorStyle' => 'wp-block-post-comments-editor', 'render_callback' => 'render_block_core_comments', 'skip_inner_blocks' => true, ); diff --git a/packages/block-library/src/cover/edit/inspector-controls.js b/packages/block-library/src/cover/edit/inspector-controls.js index d3f65672e09328..332f3f6f614cea 100644 --- a/packages/block-library/src/cover/edit/inspector-controls.js +++ b/packages/block-library/src/cover/edit/inspector-controls.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { Fragment, useMemo } from '@wordpress/element'; +import { useMemo } from '@wordpress/element'; import { Button, ExternalLink, @@ -146,7 +146,7 @@ export default function CoverInspectorControls( { { !! url && ( { isImageBackground && ( - + <> - + ) } { showFocalPointPicker && ( select( blockEditorStore ).canRemoveBlock( clientId ), + [ clientId ] + ); + const [ isIframed, setIsIframed ] = useState( false ); + const ref = useRefEffect( ( element ) => { + setIsIframed( element.ownerDocument !== document ); + }, [] ); + + return ( + <> + { canRemove && ( + + + + + + ) } +
+ { isIframed ? ( + + ) : ( + + ) } +
+ + ); +} + +function ClassicEdit( { clientId, attributes: { content }, setAttributes, onReplace, } ) { const { getMultiSelectedBlockClientIds } = useSelect( blockEditorStore ); - const canRemove = useSelect( - ( select ) => select( blockEditorStore ).canRemoveBlock( clientId ), - [ clientId ] - ); const didMount = useRef( false ); useEffect( () => { @@ -225,28 +253,19 @@ export default function ClassicEdit( { /* eslint-disable jsx-a11y/no-static-element-interactions */ return ( <> - { canRemove && ( - - - - - - ) } -
-
-
-
+
+
); /* eslint-enable jsx-a11y/no-static-element-interactions */ diff --git a/packages/block-library/src/freeform/modal.js b/packages/block-library/src/freeform/modal.js new file mode 100644 index 00000000000000..67338be91fa864 --- /dev/null +++ b/packages/block-library/src/freeform/modal.js @@ -0,0 +1,111 @@ +/** + * WordPress dependencies + */ +import { BlockControls, store } from '@wordpress/block-editor'; +import { + ToolbarGroup, + ToolbarButton, + Modal, + Button, +} from '@wordpress/components'; +import { useEffect, useState, RawHTML } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { useSelect } from '@wordpress/data'; + +function ClassicEdit( props ) { + const styles = useSelect( + ( select ) => select( store ).getSettings().styles + ); + useEffect( () => { + const { baseURL, suffix, settings } = window.wpEditorL10n.tinymce; + + window.tinymce.EditorManager.overrideDefaults( { + base_url: baseURL, + suffix, + } ); + + window.wp.oldEditor.initialize( props.id, { + tinymce: { + ...settings, + height: 500, + setup( editor ) { + editor.on( 'init', () => { + const doc = editor.getDoc(); + styles.forEach( ( { css } ) => { + const styleEl = doc.createElement( 'style' ); + styleEl.innerHTML = css; + doc.head.appendChild( styleEl ); + } ); + } ); + }, + }, + } ); + + return () => { + window.wp.oldEditor.remove( props.id ); + }; + }, [] ); + + return