From 8a1526e209bda17245ff9dc2772a53647f429175 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Wed, 7 Aug 2024 18:15:06 +0000 Subject: [PATCH] Merge changes published in the Gutenberg plugin "release/19.0" branch --- .eslintrc.js | 4 + .../workflows/check-backport-changelog.yml | 2 +- .github/workflows/performance.yml | 6 +- .stylelintrc.json | 10 +- backport-changelog/6.6/7088.md | 4 + backport-changelog/6.6/7097.md | 3 + backport-changelog/6.7/7137.md | 3 + changelog.txt | 405 ++ docs/assets/plugin-sidebar-closed-state.png | Bin 6773 -> 0 bytes docs/assets/plugin-sidebar-open-state.png | Bin 10854 -> 27542 bytes .../getting-started-with-code-contribution.md | 4 +- docs/getting-started/devenv/README.md | 1 + docs/reference-guides/core-blocks.md | 22 +- .../data/data-core-block-editor.md | 2 +- docs/reference-guides/data/data-core.md | 103 +- .../reference-guides/filters/block-filters.md | 194 +- .../interactivity-api/README.md | 2 +- .../interactivity-api/api-reference.md | 4 +- .../interactivity-api/iapi-faq.md | 2 +- .../slotfills/plugin-sidebar.md | 68 +- gutenberg.php | 4 +- lib/block-supports/block-style-variations.php | 2 +- lib/block-supports/layout.php | 25 +- lib/class-wp-theme-json-gutenberg.php | 37 +- ...class-wp-theme-json-resolver-gutenberg.php | 35 +- lib/compat/plugin/fonts.php | 43 + lib/compat/plugin/footnotes.php | 250 -- lib/compat/wordpress-6.4/block-hooks.php | 377 -- lib/compat/wordpress-6.4/blocks.php | 23 - ...tenberg-rest-block-patterns-controller.php | 46 - ...global-styles-revisions-controller-6-4.php | 164 - ...utenberg-rest-templates-controller-6-4.php | 75 - .../font-face/class-wp-font-face-resolver.php | 183 - .../fonts/font-face/class-wp-font-face.php | 435 -- lib/compat/wordpress-6.4/fonts/fonts.php | 78 - ...class-gutenberg-html-tag-processor-6-4.php | 2457 ------------ ...ass-wp-html-active-formatting-elements.php | 190 - .../html-api/class-wp-html-open-elements.php | 435 -- .../class-wp-html-processor-state.php | 146 - .../html-api/class-wp-html-processor.php | 1446 ------- .../html-api/class-wp-html-token.php | 109 - .../class-wp-html-unsupported-exception.php | 34 - lib/compat/wordpress-6.4/kses.php | 18 - lib/compat/wordpress-6.4/rest-api.php | 40 - lib/compat/wordpress-6.4/script-loader.php | 112 - lib/compat/wordpress-6.4/theme-previews.php | 27 - .../block-bindings/block-bindings.php | 89 - .../class-wp-block-bindings-registry.php | 288 -- .../class-wp-block-bindings-source.php | 99 - .../block-bindings/post-meta.php | 65 - lib/compat/wordpress-6.5/block-patterns.php | 79 - lib/compat/wordpress-6.5/blocks.php | 414 -- ...global-styles-revisions-controller-6-5.php | 111 - .../wordpress-6.5/class-wp-script-modules.php | 362 -- lib/compat/wordpress-6.5/compat.php | 92 - .../fonts/class-wp-font-collection.php | 298 -- .../fonts/class-wp-font-library.php | 145 - .../fonts/class-wp-font-utils.php | 260 -- ...ss-wp-rest-font-collections-controller.php | 326 -- .../class-wp-rest-font-faces-controller.php | 952 ----- ...class-wp-rest-font-families-controller.php | 567 --- lib/compat/wordpress-6.5/fonts/fonts.php | 485 --- ...ass-gutenberg-html-attribute-token-6-5.php | 116 - ...class-gutenberg-html-open-elements-6-5.php | 462 --- .../class-gutenberg-html-processor-6-5.php | 1929 --------- ...ass-gutenberg-html-processor-state-6-5.php | 143 - .../class-gutenberg-html-span-6-5.php | 56 - ...class-gutenberg-html-tag-processor-6-5.php | 3555 ----------------- ...ss-gutenberg-html-text-replacement-6-5.php | 64 - ...interactivity-api-directives-processor.php | 246 -- .../class-wp-interactivity-api.php | 992 ----- .../interactivity-api/interactivity-api.php | 205 - lib/compat/wordpress-6.5/kses.php | 18 - .../navigation-block-variations.php | 143 - lib/compat/wordpress-6.5/rest-api.php | 136 - lib/compat/wordpress-6.5/script-loader.php | 207 - lib/compat/wordpress-6.5/scripts-modules.php | 224 -- ...global-styles-revisions-controller-6-6.php | 2 +- ...utenberg-rest-templates-controller-6-6.php | 2 +- .../class-gutenberg-html-processor-6-6.php | 4 +- ...class-gutenberg-html-tag-processor-6-6.php | 12 +- lib/compat/wordpress-6.6/rest-api.php | 4 +- lib/compat/wordpress-6.7/block-bindings.php | 2 +- .../class-wp-rest-customizer-nonces.php | 74 - lib/experimental/editor-settings.php | 21 +- lib/experimental/posts/load.php | 2 +- lib/experimental/rest-api.php | 9 - lib/experimental/script-modules.php | 9 +- lib/experiments-page.php | 24 + lib/load.php | 97 +- package-lock.json | 261 +- package.json | 4 +- packages/base-styles/_animations.scss | 5 + packages/base-styles/_mixins.scss | 16 - packages/base-styles/_variables.scss | 17 +- packages/base-styles/_z-index.scss | 3 + packages/block-editor/CHANGELOG.md | 9 + packages/block-editor/README.md | 12 +- .../src/autocompleters/style.scss | 4 + .../src/components/block-breadcrumb/index.js | 6 +- .../src/components/block-canvas/style.scss | 1 + .../src/components/block-draggable/index.js | 6 +- .../use-block-props/use-block-refs.js | 70 +- .../src/components/block-mover/index.js | 7 +- .../src/components/block-popover/cover.js | 2 +- .../src/components/block-popover/inbetween.js | 2 +- .../src/components/block-popover/index.js | 2 +- .../block-tools/block-selection-button.js | 3 +- .../src/components/block-tools/index.js | 11 +- .../use-block-toolbar-popover-props.js | 2 +- .../block-tools/zoom-out-mode-inserters.js | 32 +- .../block-tools/zoom-out-toolbar.js | 1 - .../button-block-appender/content.scss | 2 +- .../components/child-layout-control/index.js | 2 + .../test/__snapshots__/control.js.snap | 4 +- .../colors-gradients/test/control.js | 5 +- .../components/dimensions-tool/scale-tool.js | 1 + .../src/components/font-family/README.md | 8 + .../src/components/font-family/index.js | 16 + .../font-family/stories/index.story.js | 54 + .../global-styles/background-panel.js | 130 +- .../global-styles/image-settings-panel.js | 1 + .../src/components/global-styles/style.scss | 10 - .../test/theme-file-uri-utils.js | 27 +- .../test/use-global-styles-output.js | 53 +- .../components/global-styles/test/utils.js | 120 + .../global-styles/theme-file-uri-utils.js | 59 - .../global-styles/typography-panel.js | 1 - .../global-styles/use-global-styles-output.js | 115 +- .../src/components/global-styles/utils.js | 112 + .../src/components/grid/grid-item-movers.js | 90 +- .../src/components/grid/grid-item-resizer.js | 28 +- .../src/components/grid/grid-visualizer.js | 2 +- .../src/components/grid/style.scss | 70 +- .../components/grid/use-grid-layout-sync.js | 82 +- .../src/components/iframe/content.scss | 4 +- .../src/components/iframe/index.js | 10 +- .../pattern-category-preview-panel.js | 25 - .../inserter/category-tabs/index.js | 37 +- .../src/components/inserter/menu.js | 4 +- .../src/components/inserter/style.scss | 27 +- .../components/inspector-controls/README.md | 5 + .../components/line-height-control/README.md | 8 - .../components/line-height-control/index.js | 22 +- .../stories/index.story.js | 1 - .../line-height-control/test/index.js | 8 +- .../components/link-control/search-input.js | 1 - .../components/media-replace-flow/README.md | 7 + .../components/media-replace-flow/index.js | 4 +- .../src/components/resolution-tool/index.js | 1 + .../responsive-block-control/test/index.js | 6 +- .../skip-to-selected-block/index.js | 8 +- .../input-controls/spacing-input-control.js | 9 +- .../src/components/tabbed-sidebar/style.scss | 20 +- .../src/components/url-input/README.md | 5 - .../src/components/url-input/button.js | 1 - .../src/components/url-input/index.js | 16 +- .../src/components/url-popover/link-editor.js | 1 - .../components/use-block-drop-zone/index.js | 80 +- .../src/components/use-on-block-drop/index.js | 10 +- .../src/components/use-resize-canvas/index.js | 4 +- .../src/components/use-settings/index.js | 3 +- .../block-editor/src/hooks/block-bindings.js | 355 +- .../src/hooks/block-bindings.scss | 15 +- .../block-editor/src/hooks/block-hooks.js | 1 + .../block-editor/src/hooks/block-hooks.scss | 1 + .../src/hooks/block-style-variation.js | 6 +- .../src/hooks/contrast-checker.js | 14 +- packages/block-editor/src/hooks/duotone.js | 8 +- packages/block-editor/src/hooks/index.js | 3 +- .../block-editor/src/hooks/line-height.js | 1 - .../src/hooks/spacing-visualizer.js | 2 +- .../src/hooks/use-bindings-attributes.js | 4 + .../use-editor-wrapper-styles.native.scss | 1 + .../block-editor/src/hooks/use-zoom-out.js | 2 +- packages/block-editor/src/hooks/utils.js | 15 +- packages/block-editor/src/index.js | 1 + packages/block-editor/src/private-apis.js | 3 +- packages/block-editor/src/store/actions.js | 18 +- packages/block-editor/src/store/selectors.js | 8 +- packages/block-editor/src/style.scss | 2 +- .../src/utils/get-editor-region.js | 2 +- .../src/utils/get-px-from-css-unit.js | 2 +- packages/block-library/CHANGELOG.md | 4 + packages/block-library/src/avatar/edit.js | 1 + packages/block-library/src/button/index.php | 15 +- .../block-library/src/categories/block.json | 12 + packages/block-library/src/column/block.json | 2 + packages/block-library/src/column/edit.js | 4 +- .../src/comment-template/block.json | 12 + .../src/comments-title/block.json | 3 + .../block-library/src/comments-title/edit.js | 9 +- packages/block-library/src/details/edit.js | 1 + packages/block-library/src/editor.scss | 1 + packages/block-library/src/form-input/edit.js | 1 + .../block-library/src/form-input/style.scss | 1 + packages/block-library/src/form/edit.js | 5 +- packages/block-library/src/gallery/edit.js | 88 +- .../block-library/src/gallery/editor.scss | 1 - .../block-library/src/gallery/gap-styles.js | 9 +- .../test/__snapshots__/index.native.js.snap | 2 +- .../src/gallery/test/index.native.js | 10 +- .../src/gallery/use-image-sizes.js | 4 +- packages/block-library/src/group/editor.scss | 2 +- packages/block-library/src/group/index.js | 55 +- .../block-library/src/group/variations.js | 56 + packages/block-library/src/heading/block.json | 3 + packages/block-library/src/heading/edit.js | 4 +- packages/block-library/src/image/editor.scss | 12 + packages/block-library/src/image/view.js | 2 +- .../block-library/src/latest-posts/edit.js | 1 + .../src/latest-posts/editor.scss | 4 - .../block-library/src/list-item/block.json | 5 +- packages/block-library/src/list/block.json | 2 +- .../src/list/ordered-list-settings.js | 38 +- .../src/list/test/edit.native.js | 4 +- .../block-library/src/loginout/block.json | 15 +- .../block-library/src/loginout/style.scss | 4 + .../src/media-text/style.native.scss | 2 + .../src/navigation/edit/index.js | 10 +- .../block-library/src/navigation/editor.scss | 1 - .../src/post-comments-form/block.json | 12 + .../block-library/src/post-content/block.json | 1 + .../block-library/src/post-content/style.scss | 5 + .../block-library/src/post-date/block.json | 12 + .../block-library/src/post-excerpt/block.json | 12 + .../block-library/src/post-excerpt/style.scss | 3 + .../src/post-navigation-link/edit.js | 2 + .../block-library/src/post-terms/block.json | 12 + .../src/post-time-to-read/block.json | 6 + .../block-library/src/post-title/block.json | 15 + packages/block-library/src/post-title/edit.js | 7 +- .../block-library/src/query-title/block.json | 3 + .../block-library/src/query-title/edit.js | 10 +- .../enhanced-pagination-control.js | 1 + .../query/edit/inspector-controls/index.js | 97 +- .../inspector-controls/offset-controls.js | 31 + .../edit/inspector-controls/pages-control.js | 27 + .../inspector-controls/per-page-control.js | 33 + .../edit/inspector-controls/sticky-control.js | 2 +- .../src/query/edit/query-toolbar.js | 91 +- packages/block-library/src/query/utils.js | 14 +- packages/block-library/src/quote/theme.scss | 8 +- packages/block-library/src/site-logo/edit.js | 32 +- .../block-library/src/site-tagline/block.json | 13 +- .../block-library/src/site-tagline/edit.js | 6 +- .../block-library/src/site-tagline/style.scss | 4 + .../block-library/src/site-title/block.json | 10 + packages/block-library/src/site-title/edit.js | 6 +- .../block-library/src/site-title/style.scss | 9 +- .../block-library/src/social-link/edit.js | 1 - packages/block-library/src/spacer/controls.js | 23 +- packages/block-library/src/style.scss | 4 + .../src/table-of-contents/block.json | 15 +- .../src/table-of-contents/style.scss | 4 + .../block-library/src/tag-cloud/block.json | 2 +- packages/block-library/src/tag-cloud/edit.js | 80 +- .../block-library/src/tag-cloud/editor.scss | 9 + .../src/template-part/edit/index.js | 17 +- .../src/template-part/edit/inner-blocks.js | 10 +- packages/block-library/src/video/editor.scss | 4 - packages/blocks/src/api/registration.js | 9 + packages/blocks/src/api/test/registration.js | 54 + packages/blocks/src/store/private-actions.js | 1 + packages/blocks/src/store/reducer.js | 48 +- packages/blocks/src/store/test/reducer.js | 16 + packages/components/CHANGELOG.md | 30 +- packages/components/package.json | 2 +- .../alignment-matrix-control/test/index.tsx | 4 +- .../src/autocomplete/autocompleter-ui.tsx | 2 + .../components/src/autocomplete/style.scss | 6 - .../styles/base-control-styles.ts | 2 +- .../components/src/border-control/styles.ts | 5 - .../src/button/stories/e2e/index.story.tsx | 124 +- packages/components/src/button/style.scss | 70 +- packages/components/src/button/test/index.tsx | 58 +- .../src/circular-option-picker/test/index.tsx | 5 +- .../components/src/color-palette/index.tsx | 42 +- .../src/composite/legacy/test/index.tsx | 20 +- .../src/custom-select-control-v2/styles.ts | 13 +- .../custom-select-control-v2/test/index.tsx | 34 +- .../src/custom-select-control/index.tsx | 80 +- .../src/custom-select-control/test/index.tsx | 77 +- .../src/custom-select-control/types.ts | 14 +- packages/components/src/date-time/index.ts | 3 +- .../date-time/stories/time-input.story.tsx | 36 - .../src/date-time/stories/time.story.tsx | 17 + .../components/src/date-time/time/index.tsx | 62 +- .../components/src/date-time/time/styles.ts | 1 + .../date-time/{ => time}/time-input/index.tsx | 18 +- .../{ => time}/time-input/test/index.tsx | 0 .../components/src/dropdown-menu-v2/styles.ts | 35 +- .../src/dropdown-menu-v2/test/index.tsx | 5 +- .../src/font-size-picker/test/index.tsx | 93 +- packages/components/src/form-toggle/index.tsx | 44 +- packages/components/src/guide/index.tsx | 2 + packages/components/src/heading/types.ts | 5 +- packages/components/src/modal/index.tsx | 41 +- .../components/src/placeholder/style.scss | 13 +- .../components/src/query-controls/index.tsx | 6 +- .../components/src/radio-control/index.tsx | 55 +- .../src/radio-control/stories/index.story.tsx | 23 + .../components/src/radio-control/style.scss | 28 +- .../src/radio-control/test/index.tsx | 274 ++ .../components/src/radio-control/types.ts | 4 + .../components/src/select-control/README.md | 9 +- .../components/src/select-control/index.tsx | 55 +- .../select-control/test/select-control.tsx | 152 +- .../components/src/select-control/types.ts | 135 +- .../components/src/tab-panel/test/index.tsx | 9 +- packages/components/src/tabs/test/index.tsx | 152 +- .../components/src/text-control/README.md | 1 + .../components/src/text-control/index.tsx | 1 + .../components/src/text-control/style.scss | 5 + .../components/src/toggle-control/README.md | 9 + .../components/src/toggle-control/index.tsx | 47 +- .../components/src/toggle-control/style.scss | 3 +- .../test/__snapshots__/index.tsx.snap | 12 +- .../src/toggle-group-control/test/index.tsx | 6 - .../README.md | 2 +- .../component.tsx | 28 +- .../toggle-group-control-option/README.md | 7 +- .../toggle-group-control-option/component.tsx | 7 +- .../toggle-group-control/README.md | 14 +- .../toggle-group-control/component.tsx | 7 +- packages/components/src/tooltip/index.tsx | 17 +- .../components/src/tooltip/test/index.tsx | 5 - .../src/tooltip/test/utils/index.tsx | 10 +- packages/components/src/tree-select/index.tsx | 3 +- .../components/src/utils/config-values.js | 6 + packages/core-data/README.md | 2 +- packages/core-data/src/actions.js | 2 +- .../footnotes/get-rich-text-values-cached.js | 2 +- .../core-data/src/hooks/use-entity-records.ts | 50 + packages/core-data/src/index.js | 3 +- packages/core-data/src/lock-unlock.js | 10 + packages/core-data/src/private-apis.js | 14 +- packages/core-data/src/private-selectors.ts | 43 + packages/dataviews/CHANGELOG.md | 19 +- packages/dataviews/package.json | 3 +- .../src/components/dataform/index.tsx | 105 +- .../dataform/stories/index.story.tsx | 43 +- .../dataviews-bulk-actions/index.tsx | 5 + .../dataviews-filters/add-filter.tsx | 58 +- .../components/dataviews-filters/index.tsx | 210 +- .../components/dataviews-filters/style.scss | 30 + .../src/components/dataviews-layout/index.tsx | 2 +- .../src/components/dataviews-search/index.tsx | 13 +- .../dataviews-view-config/index.tsx | 530 +-- .../dataviews-view-config/style.scss | 44 + .../src/components/dataviews/index.tsx | 28 +- .../components/dataviews/stories/fixtures.js | 1 + .../src/components/dataviews/style.scss | 7 +- packages/dataviews/src/constants.ts | 5 + .../dataviews/src/dataforms-layouts/index.tsx | 20 + .../src/dataforms-layouts/panel/index.tsx | 164 + .../src/dataforms-layouts/panel/style.scss | 59 + .../src/dataforms-layouts/regular/index.tsx | 41 + .../grid/density-picker.tsx | 4 +- .../grid/index.tsx | 16 +- .../grid/style.scss | 29 + .../{layouts => dataviews-layouts}/index.ts | 0 .../list/index.tsx | 0 .../list/style.scss | 5 +- .../table/column-header-menu.tsx | 2 +- .../table/index.tsx | 0 .../table/style.scss | 0 packages/dataviews/src/field-types/index.tsx | 45 + .../dataviews/src/field-types/integer.tsx | 103 + packages/dataviews/src/field-types/text.tsx | 95 + .../src/filter-and-sort-data-view.ts | 16 +- packages/dataviews/src/index.ts | 3 +- packages/dataviews/src/normalize-fields.ts | 47 +- packages/dataviews/src/style.scss | 9 +- .../src/test/filter-and-sort-data-view.js | 49 +- packages/dataviews/src/test/validation.ts | 131 + packages/dataviews/src/types.ts | 53 +- packages/dataviews/src/validation.ts | 18 + packages/dataviews/tsconfig.json | 3 +- .../plugins/delete-installed-fonts.php | 2 +- .../directive-each/render.php | 4 +- .../interactive-blocks/directive-each/view.js | 10 +- .../directive-priorities/view.js | 10 +- packages/e2e-tests/plugins/observe-typing.php | 28 + .../e2e-tests/plugins/observe-typing/index.js | 45 + .../components/init-pattern-modal/index.js | 1 + packages/edit-post/src/index.js | 2 - .../src/components/add-new-pattern/index.js | 11 +- .../src/components/add-new-post/index.js | 3 +- .../src/components/add-new-template/utils.js | 16 +- .../edit-site/src/components/editor/index.js | 102 +- .../src/components/editor/style.scss | 60 +- .../components/global-styles/font-families.js | 8 +- .../font-library-modal/context.js | 5 - .../font-library-modal/font-collection.js | 44 +- .../global-styles/font-library-modal/index.js | 9 +- .../font-library-modal/installed-fonts.js | 28 +- .../font-library-modal/style.scss | 2 +- .../font-library-modal/upload-fonts.js | 4 +- .../global-styles/screen-typeset.js | 42 + .../screen-typography-element.js | 14 + .../global-styles/screen-typography.js | 8 +- .../global-styles/shadows-edit-panel.js | 139 +- .../src/components/global-styles/style.scss | 11 +- .../global-styles/typeset-button.js | 93 + .../src/components/global-styles/typeset.js | 73 + .../src/components/global-styles/ui.js | 5 + .../src/components/global-styles/utils.js | 14 +- .../src/components/layout/style.scss | 8 + .../src/components/page-patterns/fields.js | 251 ++ .../src/components/page-patterns/index.js | 259 +- .../src/components/page-patterns/style.scss | 167 +- .../components/page-patterns/use-patterns.js | 32 +- .../src/components/page-templates/fields.js | 157 + .../src/components/page-templates/index.js | 189 +- .../src/components/page-templates/style.scss | 19 +- .../src/components/post-edit/index.js | 35 +- .../src/components/post-fields/index.js | 46 +- .../src/components/post-list/index.js | 208 +- .../sidebar-dataviews/add-new-view.js | 6 +- .../sidebar-dataviews/dataview-item.js | 4 +- .../sidebar-dataviews/default-views.js | 190 +- .../src/components/sidebar-dataviews/index.js | 40 +- .../src/components/site-hub/index.js | 13 +- .../src/components/site-icon/style.scss | 5 +- .../src/components/style-book/index.js | 59 +- .../use-init-edited-entity-from-url.js | 5 + .../push-changes-to-global-styles/index.js | 5 +- .../use-theme-style-variations-by-property.js | 5 +- packages/edit-site/src/index.js | 2 - packages/edit-site/src/utils/clone-deep.js | 8 - packages/editor/src/bindings/post-meta.js | 20 + .../editor/src/components/blog-title/index.js | 2 +- .../editor/src/components/commands/index.js | 6 +- .../src/components/document-bar/index.js | 2 +- .../src/components/document-bar/style.scss | 2 +- .../test/__snapshots__/index.js.snap | 111 - .../components/document-outline/test/index.js | 185 - .../components/editor-interface/style.scss | 5 + .../editor/src/components/header/index.js | 3 +- .../components/list-view-sidebar/style.scss | 4 + .../src/components/page-attributes/parent.js | 32 +- .../src/components/post-actions/actions.js | 517 +-- .../src/components/post-actions/index.js | 26 +- .../editor/src/components/post-author/hook.js | 14 +- .../src/components/post-author/panel.js | 4 +- .../src/components/post-card-panel/index.js | 2 +- .../src/components/post-comments/index.js | 27 +- .../test/__snapshots__/index.js.snap | 4 +- .../src/components/post-status/index.js | 52 +- .../create-new-template-modal.js | 8 +- .../editor/src/components/post-url/panel.js | 10 +- .../src/components/preview-dropdown/index.js | 148 +- .../components/preview-dropdown/style.scss | 5 + .../disable-non-page-content-blocks.js | 51 +- .../src/components/site-discussion/index.js | 26 +- .../template-content-panel/index.js | 57 +- .../src/components/visual-editor/index.js | 14 +- .../src/components/visual-editor/style.scss | 4 +- .../actions/export-pattern.native.tsx | 3 + .../editor/src/dataviews/actions/index.ts | 25 - .../actions/permanently-delete-post.tsx | 116 + .../src/dataviews/actions/restore-post.tsx | 134 + .../src/dataviews/actions/trash-post.tsx | 196 + .../src/dataviews/store/private-actions.ts | 68 + .../src/dataviews/store/private-selectors.ts | 26 +- .../editor/src/dataviews/store/reducer.ts | 21 +- packages/editor/src/dataviews/types.ts | 11 + packages/editor/src/private-apis.js | 2 - packages/editor/src/store/actions.js | 49 +- packages/editor/src/store/private-actions.js | 12 +- .../editor/src/store/private-selectors.js | 47 +- .../src/store/test/private-selectors.js | 78 + packages/element/src/react.js | 16 +- packages/format-library/src/image/index.js | 69 +- packages/format-library/src/image/style.scss | 13 +- packages/format-library/src/language/index.js | 12 +- packages/icons/CHANGELOG.md | 4 + packages/icons/src/index.js | 2 + packages/icons/src/library/home-button.js | 16 + packages/icons/src/library/send.js | 16 + packages/icons/src/library/sides-axial.js | 7 +- packages/icons/src/library/sides-bottom.js | 2 +- packages/icons/src/library/styles.js | 6 +- packages/interactivity-router/src/index.ts | 8 +- packages/interactivity/package.json | 1 - packages/interactivity/src/directives.tsx | 154 +- packages/interactivity/src/hooks.tsx | 93 +- packages/interactivity/src/index.ts | 21 +- packages/interactivity/src/namespaces.ts | 10 + packages/interactivity/src/proxies/index.ts | 5 + .../interactivity/src/proxies/registry.ts | 82 + packages/interactivity/src/proxies/signals.ts | 143 + packages/interactivity/src/proxies/state.ts | 250 ++ packages/interactivity/src/proxies/store.ts | 79 + .../src/proxies/test/state-proxy.ts | 1269 ++++++ .../src/proxies/test/store-proxy.ts | 123 + packages/interactivity/src/scopes.ts | 98 + packages/interactivity/src/store.ts | 192 +- packages/interactivity/src/test/utils.ts | 337 +- packages/interactivity/src/utils.ts | 67 +- .../components/interface-skeleton/index.js | 16 +- .../src/components/create-pattern-modal.js | 1 + packages/primitives/package.json | 3 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 63375 -> 43504 bytes .../gradle/wrapper/gradle-wrapper.properties | 3 +- packages/react-native-aztec/android/gradlew | 22 +- .../react-native-aztec/android/gradlew.bat | 22 +- .../android/settings.gradle | 2 +- .../android/gradle/wrapper/gradle-wrapper.jar | Bin 63375 -> 43504 bytes .../gradle/wrapper/gradle-wrapper.properties | 3 +- packages/react-native-bridge/android/gradlew | 22 +- .../react-native-bridge/android/gradlew.bat | 22 +- .../android/settings.gradle | 2 +- .../react-native-editor/android/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 3 +- packages/react-native-editor/android/gradlew | 5 +- .../react-native-editor/android/gradlew.bat | 2 +- .../reusable-block-convert-button.js | 1 + phpunit/bootstrap.php | 1 + ...lobal-styles-revisions-controller-test.php | 351 -- phpunit/class-wp-theme-json-test.php | 215 +- .../fontFamilyBackwardsCompatibility.php | 196 - readme.txt | 2 +- schemas/json/wp-env.json | 4 +- .../various/adding-inline-tokens.spec.js | 9 +- .../editor/various/block-bindings.spec.js | 182 +- .../editor/various/change-detection.spec.js | 2 +- .../specs/editor/various/is-typing.spec.js | 40 +- test/e2e/specs/editor/various/sidebar.spec.js | 2 +- .../interactivity/deferred-store.spec.ts | 4 +- .../interactivity/directive-context.spec.ts | 50 +- .../interactivity/directive-each.spec.ts | 9 +- .../interactivity/router-navigate.spec.ts | 31 +- .../dataviews-list-layout-keyboard.spec.js | 18 +- .../site-editor/new-templates-list.spec.js | 8 +- test/e2e/specs/site-editor/patterns.spec.js | 8 + .../fixtures/blocks/core__site-tagline.json | 3 +- .../fixtures/blocks/core__site-title.json | 1 + test/integration/wp-env-schema.test.js | 38 + test/performance/specs/site-editor.spec.js | 4 +- test/unit/jest.config.js | 2 +- 542 files changed, 12179 insertions(+), 25523 deletions(-) create mode 100644 backport-changelog/6.6/7088.md create mode 100644 backport-changelog/6.6/7097.md create mode 100644 backport-changelog/6.7/7137.md delete mode 100644 docs/assets/plugin-sidebar-closed-state.png create mode 100644 lib/compat/plugin/fonts.php delete mode 100644 lib/compat/plugin/footnotes.php delete mode 100644 lib/compat/wordpress-6.4/block-hooks.php delete mode 100644 lib/compat/wordpress-6.4/blocks.php delete mode 100644 lib/compat/wordpress-6.4/class-gutenberg-rest-block-patterns-controller.php delete mode 100644 lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php delete mode 100644 lib/compat/wordpress-6.4/class-gutenberg-rest-templates-controller-6-4.php delete mode 100644 lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face-resolver.php delete mode 100644 lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face.php delete mode 100644 lib/compat/wordpress-6.4/fonts/fonts.php delete mode 100644 lib/compat/wordpress-6.4/html-api/class-gutenberg-html-tag-processor-6-4.php delete mode 100644 lib/compat/wordpress-6.4/html-api/class-wp-html-active-formatting-elements.php delete mode 100644 lib/compat/wordpress-6.4/html-api/class-wp-html-open-elements.php delete mode 100644 lib/compat/wordpress-6.4/html-api/class-wp-html-processor-state.php delete mode 100644 lib/compat/wordpress-6.4/html-api/class-wp-html-processor.php delete mode 100644 lib/compat/wordpress-6.4/html-api/class-wp-html-token.php delete mode 100644 lib/compat/wordpress-6.4/html-api/class-wp-html-unsupported-exception.php delete mode 100644 lib/compat/wordpress-6.4/kses.php delete mode 100644 lib/compat/wordpress-6.4/rest-api.php delete mode 100644 lib/compat/wordpress-6.4/script-loader.php delete mode 100644 lib/compat/wordpress-6.4/theme-previews.php delete mode 100644 lib/compat/wordpress-6.5/block-bindings/block-bindings.php delete mode 100644 lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings-registry.php delete mode 100644 lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings-source.php delete mode 100644 lib/compat/wordpress-6.5/block-bindings/post-meta.php delete mode 100644 lib/compat/wordpress-6.5/block-patterns.php delete mode 100644 lib/compat/wordpress-6.5/blocks.php delete mode 100644 lib/compat/wordpress-6.5/class-gutenberg-rest-global-styles-revisions-controller-6-5.php delete mode 100644 lib/compat/wordpress-6.5/class-wp-script-modules.php delete mode 100644 lib/compat/wordpress-6.5/compat.php delete mode 100644 lib/compat/wordpress-6.5/fonts/class-wp-font-collection.php delete mode 100644 lib/compat/wordpress-6.5/fonts/class-wp-font-library.php delete mode 100644 lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php delete mode 100644 lib/compat/wordpress-6.5/fonts/class-wp-rest-font-collections-controller.php delete mode 100644 lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php delete mode 100644 lib/compat/wordpress-6.5/fonts/class-wp-rest-font-families-controller.php delete mode 100644 lib/compat/wordpress-6.5/fonts/fonts.php delete mode 100644 lib/compat/wordpress-6.5/html-api/class-gutenberg-html-attribute-token-6-5.php delete mode 100644 lib/compat/wordpress-6.5/html-api/class-gutenberg-html-open-elements-6-5.php delete mode 100644 lib/compat/wordpress-6.5/html-api/class-gutenberg-html-processor-6-5.php delete mode 100644 lib/compat/wordpress-6.5/html-api/class-gutenberg-html-processor-state-6-5.php delete mode 100644 lib/compat/wordpress-6.5/html-api/class-gutenberg-html-span-6-5.php delete mode 100644 lib/compat/wordpress-6.5/html-api/class-gutenberg-html-tag-processor-6-5.php delete mode 100644 lib/compat/wordpress-6.5/html-api/class-gutenberg-html-text-replacement-6-5.php delete mode 100644 lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api-directives-processor.php delete mode 100644 lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php delete mode 100644 lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php delete mode 100644 lib/compat/wordpress-6.5/kses.php delete mode 100644 lib/compat/wordpress-6.5/navigation-block-variations.php delete mode 100644 lib/compat/wordpress-6.5/rest-api.php delete mode 100644 lib/compat/wordpress-6.5/script-loader.php delete mode 100644 lib/compat/wordpress-6.5/scripts-modules.php delete mode 100644 lib/experimental/class-wp-rest-customizer-nonces.php create mode 100644 packages/block-editor/src/components/font-family/stories/index.story.js delete mode 100644 packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-preview-panel.js create mode 100644 packages/block-library/src/loginout/style.scss create mode 100644 packages/block-library/src/post-content/style.scss create mode 100644 packages/block-library/src/query/edit/inspector-controls/offset-controls.js create mode 100644 packages/block-library/src/query/edit/inspector-controls/pages-control.js create mode 100644 packages/block-library/src/query/edit/inspector-controls/per-page-control.js create mode 100644 packages/block-library/src/site-tagline/style.scss create mode 100644 packages/block-library/src/table-of-contents/style.scss create mode 100644 packages/block-library/src/tag-cloud/editor.scss delete mode 100644 packages/components/src/date-time/stories/time-input.story.tsx rename packages/components/src/date-time/{ => time}/time-input/index.tsx (91%) rename packages/components/src/date-time/{ => time}/time-input/test/index.tsx (100%) create mode 100644 packages/components/src/radio-control/test/index.tsx create mode 100644 packages/core-data/src/lock-unlock.js create mode 100644 packages/dataviews/src/components/dataviews-view-config/style.scss create mode 100644 packages/dataviews/src/dataforms-layouts/index.tsx create mode 100644 packages/dataviews/src/dataforms-layouts/panel/index.tsx create mode 100644 packages/dataviews/src/dataforms-layouts/panel/style.scss create mode 100644 packages/dataviews/src/dataforms-layouts/regular/index.tsx rename packages/dataviews/src/{layouts => dataviews-layouts}/grid/density-picker.tsx (97%) rename packages/dataviews/src/{layouts => dataviews-layouts}/grid/index.tsx (96%) rename packages/dataviews/src/{layouts => dataviews-layouts}/grid/style.scss (75%) rename packages/dataviews/src/{layouts => dataviews-layouts}/index.ts (100%) rename packages/dataviews/src/{layouts => dataviews-layouts}/list/index.tsx (100%) rename packages/dataviews/src/{layouts => dataviews-layouts}/list/style.scss (97%) rename packages/dataviews/src/{layouts => dataviews-layouts}/table/column-header-menu.tsx (99%) rename packages/dataviews/src/{layouts => dataviews-layouts}/table/index.tsx (100%) rename packages/dataviews/src/{layouts => dataviews-layouts}/table/style.scss (100%) create mode 100644 packages/dataviews/src/field-types/index.tsx create mode 100644 packages/dataviews/src/field-types/integer.tsx create mode 100644 packages/dataviews/src/field-types/text.tsx create mode 100644 packages/dataviews/src/test/validation.ts create mode 100644 packages/dataviews/src/validation.ts create mode 100644 packages/e2e-tests/plugins/observe-typing.php create mode 100644 packages/e2e-tests/plugins/observe-typing/index.js create mode 100644 packages/edit-site/src/components/global-styles/screen-typeset.js create mode 100644 packages/edit-site/src/components/global-styles/typeset-button.js create mode 100644 packages/edit-site/src/components/global-styles/typeset.js create mode 100644 packages/edit-site/src/components/page-patterns/fields.js create mode 100644 packages/edit-site/src/components/page-templates/fields.js delete mode 100644 packages/edit-site/src/utils/clone-deep.js delete mode 100644 packages/editor/src/components/document-outline/test/__snapshots__/index.js.snap delete mode 100644 packages/editor/src/components/document-outline/test/index.js create mode 100644 packages/editor/src/dataviews/actions/export-pattern.native.tsx delete mode 100644 packages/editor/src/dataviews/actions/index.ts create mode 100644 packages/editor/src/dataviews/actions/permanently-delete-post.tsx create mode 100644 packages/editor/src/dataviews/actions/restore-post.tsx create mode 100644 packages/editor/src/dataviews/actions/trash-post.tsx create mode 100644 packages/editor/src/store/test/private-selectors.js create mode 100644 packages/icons/src/library/home-button.js create mode 100644 packages/icons/src/library/send.js create mode 100644 packages/interactivity/src/namespaces.ts create mode 100644 packages/interactivity/src/proxies/index.ts create mode 100644 packages/interactivity/src/proxies/registry.ts create mode 100644 packages/interactivity/src/proxies/signals.ts create mode 100644 packages/interactivity/src/proxies/state.ts create mode 100644 packages/interactivity/src/proxies/store.ts create mode 100644 packages/interactivity/src/proxies/test/state-proxy.ts create mode 100644 packages/interactivity/src/proxies/test/store-proxy.ts create mode 100644 packages/interactivity/src/scopes.ts delete mode 100644 phpunit/class-gutenberg-rest-global-styles-revisions-controller-test.php delete mode 100644 phpunit/tests/fonts/font-library/fontFamilyBackwardsCompatibility.php create mode 100644 test/integration/wp-env-schema.test.js diff --git a/.eslintrc.js b/.eslintrc.js index 01f6967e8fe94..81408499bd34f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -295,7 +295,11 @@ module.exports = { 'FocalPointPicker', 'RangeControl', 'SearchControl', + 'SelectControl', + 'TextControl', 'TextareaControl', + 'ToggleControl', + 'ToggleGroupControl', 'TreeSelect', ].map( ( componentName ) => ( { selector: `JSXOpeningElement[name.name="${ componentName }"]:not(:has(JSXAttribute[name.name="__nextHasNoMarginBottom"]))`, diff --git a/.github/workflows/check-backport-changelog.yml b/.github/workflows/check-backport-changelog.yml index 606ca4c91683c..366bad9fdbc24 100644 --- a/.github/workflows/check-backport-changelog.yml +++ b/.github/workflows/check-backport-changelog.yml @@ -1,4 +1,4 @@ -name: Verify Core Backport Changlog +name: Verify Core Backport Changelog on: pull_request: diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 288bef1580038..98615b93b8a17 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -69,13 +69,13 @@ jobs: - name: Compare performance with base branch if: github.event_name == 'push' # The base hash used here need to be a commit that is compatible with the current WP version - # The current one is 9725060a5b18904c6cc5fdbe4b06fbde7419e02c and it needs to be updated every WP major release. + # The current one is 5f4c9c853b15092ed885d5280edefb973c37d9e9 and it needs to be updated every WP major release. # It is used as a base comparison point to avoid fluctuation in the performance metrics. run: | WP_VERSION=$(awk -F ': ' '/^Tested up to/{print $2}' readme.txt) IFS=. read -ra WP_VERSION_ARRAY <<< "$WP_VERSION" WP_MAJOR="${WP_VERSION_ARRAY[0]}.${WP_VERSION_ARRAY[1]}" - ./bin/plugin/cli.js perf $GITHUB_SHA 9725060a5b18904c6cc5fdbe4b06fbde7419e02c --tests-branch $GITHUB_SHA --wp-version "$WP_MAJOR" + ./bin/plugin/cli.js perf $GITHUB_SHA 5f4c9c853b15092ed885d5280edefb973c37d9e9 --tests-branch $GITHUB_SHA --wp-version "$WP_MAJOR" - name: Compare performance with custom branches if: github.event_name == 'workflow_dispatch' @@ -101,7 +101,7 @@ jobs: CODEHEALTH_PROJECT_TOKEN: ${{ secrets.CODEHEALTH_PROJECT_TOKEN }} run: | COMMITTED_AT=$(git show -s $GITHUB_SHA --format="%cI") - ./bin/log-performance-results.js $CODEHEALTH_PROJECT_TOKEN trunk $GITHUB_SHA 9725060a5b18904c6cc5fdbe4b06fbde7419e02c $COMMITTED_AT + ./bin/log-performance-results.js $CODEHEALTH_PROJECT_TOKEN trunk $GITHUB_SHA 5f4c9c853b15092ed885d5280edefb973c37d9e9 $COMMITTED_AT - name: Archive debug artifacts (screenshots, HTML snapshots) uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 diff --git a/.stylelintrc.json b/.stylelintrc.json index df01978222e63..663befa2e4ce0 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -4,6 +4,14 @@ "at-rule-empty-line-before": null, "at-rule-no-unknown": null, "comment-empty-line-before": null, + "declaration-property-value-allowed-list": [ + { + "flex-direction": "/^(?!(row|column)-reverse).*$/" + }, + { + "message": "Avoid the flex-direction reverse values. For accessibility reasons, visual, reading, and DOM order must match. Only use the reverse values when they do not affect reading order, meaning, and interaction." + } + ], "declaration-property-value-disallowed-list": [ { "/.*/": [ "/--wp-components-color-/" ] @@ -18,7 +26,7 @@ "property-disallowed-list": [ [ "order" ], { - "message": "Avoid the order property. For accessibility reasons, visual, reading, and DOM order must match. Only use the order property when it does not affect reading order, meaning, and interaction" + "message": "Avoid the order property. For accessibility reasons, visual, reading, and DOM order must match. Only use the order property when it does not affect reading order, meaning, and interaction." } ], "rule-empty-line-before": null, diff --git a/backport-changelog/6.6/7088.md b/backport-changelog/6.6/7088.md new file mode 100644 index 0000000000000..46bd114746484 --- /dev/null +++ b/backport-changelog/6.6/7088.md @@ -0,0 +1,4 @@ +https://github.com/WordPress/wordpress-develop/pull/7088 + +* https://github.com/WordPress/gutenberg/pull/63918 + diff --git a/backport-changelog/6.6/7097.md b/backport-changelog/6.6/7097.md new file mode 100644 index 0000000000000..e674d5ea76ba6 --- /dev/null +++ b/backport-changelog/6.6/7097.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7097 + +* https://github.com/WordPress/gutenberg/pull/63980 diff --git a/backport-changelog/6.7/7137.md b/backport-changelog/6.7/7137.md new file mode 100644 index 0000000000000..834cb29a21e6d --- /dev/null +++ b/backport-changelog/6.7/7137.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7137 + +* https://github.com/WordPress/gutenberg/pull/64192 diff --git a/changelog.txt b/changelog.txt index 8e63d9a2d1f44..b7bbdf821f374 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,410 @@ == Changelog == += 18.9.0 = + +## Changelog + +### Enhancements + +#### Block Library +- Add Clear button for Overlay color option in Cover Block. ([63580](https://github.com/WordPress/gutenberg/pull/63580)) +- Embeds: Add 'Embed' to title for clarity. ([63371](https://github.com/WordPress/gutenberg/pull/63371)) +- Columns block: Fix block preview. ([63609](https://github.com/WordPress/gutenberg/pull/63609)) +- Gallery: Add border block support. ([63428](https://github.com/WordPress/gutenberg/pull/63428)) +- Image block: Show placeholder when uploading HEIC files. ([63643](https://github.com/WordPress/gutenberg/pull/63643)) +- Latest comments: Add color block support. ([63419](https://github.com/WordPress/gutenberg/pull/63419)) +- Media Text: Add border support. ([63542](https://github.com/WordPress/gutenberg/pull/63542)) +- Polish create template part modal. ([63617](https://github.com/WordPress/gutenberg/pull/63617)) +- Post Author blocks: Add example and preview. ([62978](https://github.com/WordPress/gutenberg/pull/62978)) +- Post date block: Add a block example. ([63368](https://github.com/WordPress/gutenberg/pull/63368)) +- Post featured image: Add example of the block. ([63011](https://github.com/WordPress/gutenberg/pull/63011)) +- Post terms block: Add an example. ([63369](https://github.com/WordPress/gutenberg/pull/63369)) +- Query Loop Block: Remove Posts List variation. ([63404](https://github.com/WordPress/gutenberg/pull/63404)) +- Query Loop block: Convert the post content type setting to a ToggleGroupControl if there are few items. ([63375](https://github.com/WordPress/gutenberg/pull/63375)) +- Query Loop: Change default query loop variations. ([63353](https://github.com/WordPress/gutenberg/pull/63353)) +- Set query loop to have the inherit value by default. ([63362](https://github.com/WordPress/gutenberg/pull/63362)) +- Social Links: Add border block support. ([63629](https://github.com/WordPress/gutenberg/pull/63629)) +- Social Links: Updated soundcloud icon for social link block. ([63504](https://github.com/WordPress/gutenberg/pull/63504)) +- Social Links: Update Facebook's color to match brand guidelines. ([60424](https://github.com/WordPress/gutenberg/pull/60424)) +- Term Description: Add border block support. ([63630](https://github.com/WordPress/gutenberg/pull/63630)) + +#### Design Tools +- Background Image: Make panel appear in a consistent location. ([63551](https://github.com/WordPress/gutenberg/pull/63551)) +- Buttons: Add border, color, and padding block supports. ([63538](https://github.com/WordPress/gutenberg/pull/63538)) +- Heading: Add border support. ([63539](https://github.com/WordPress/gutenberg/pull/63539)) +- Image: Adopt margin block support. ([63546](https://github.com/WordPress/gutenberg/pull/63546)) +- Paragraph: Add border support. ([63543](https://github.com/WordPress/gutenberg/pull/63543)) +- Quote: Add border support. ([63544](https://github.com/WordPress/gutenberg/pull/63544)) +- Quote: Add spacing supports. ([63545](https://github.com/WordPress/gutenberg/pull/63545)) +- Search: Add margin support. ([63547](https://github.com/WordPress/gutenberg/pull/63547)) + +#### Data Views +- DataViews: Allow column re-ordering. ([63416](https://github.com/WordPress/gutenberg/pull/63416)) +- DataViews: Update pagination icons. ([63594](https://github.com/WordPress/gutenberg/pull/63594)) +- DataViews: Rename the header property of fields to label. ([63843](https://github.com/WordPress/gutenberg/pull/63843)) +- DataViews: Support combined fields. ([63236](https://github.com/WordPress/gutenberg/pull/63236)) +- Dataviews List: Update item layout. ([63299](https://github.com/WordPress/gutenberg/pull/63299)) +- Increase column-gap between fields in List layout. ([63603](https://github.com/WordPress/gutenberg/pull/63603)) +- Update 'Front page' badge. ([63752](https://github.com/WordPress/gutenberg/pull/63752)) +- Update: Pages: Trash view should default to table layout try 2. ([63652](https://github.com/WordPress/gutenberg/pull/63652)) +- Update: Grid layout: Allow users to adjust grid density. ([63367](https://github.com/WordPress/gutenberg/pull/63367)) +- Update: Include avatars on list view. ([63309](https://github.com/WordPress/gutenberg/pull/63309)) +- Update: List / Table layout – selected item stroke should be tinted blue. ([63312](https://github.com/WordPress/gutenberg/pull/63312)) +- Update: Make changing order an action on the ellipsis menu. ([62189](https://github.com/WordPress/gutenberg/pull/62189)) + +#### Global Styles +- Add colors and typograpghy to the browse styles section. ([63173](https://github.com/WordPress/gutenberg/pull/63173)) +- Adding Font size presets UI. ([63057](https://github.com/WordPress/gutenberg/pull/63057)) +- Apply same styles to block previews on inserter and Global Styles. ([63177](https://github.com/WordPress/gutenberg/pull/63177)) +- Background: Add background attachment to top level styles. ([61382](https://github.com/WordPress/gutenberg/pull/61382)) +- Move background panel under color panel. ([63888](https://github.com/WordPress/gutenberg/pull/63888)) + +#### Block Editor +- Hide source filter in my patterns. ([63831](https://github.com/WordPress/gutenberg/pull/63831)) +- List View: Remove the sticky position icon tooltip. ([63850](https://github.com/WordPress/gutenberg/pull/63850)) +- Patterns: Render draggable only when enabled. ([63715](https://github.com/WordPress/gutenberg/pull/63715)) + +#### Post Editor +- Add post status icon in post summary. ([63658](https://github.com/WordPress/gutenberg/pull/63658)) +- Editor: Improve Header layout. ([62636](https://github.com/WordPress/gutenberg/pull/62636)) +- Post Actions: Use entity details for capability checks. ([63423](https://github.com/WordPress/gutenberg/pull/63423)) + +#### Font Library +- Group fonts by source. ([63211](https://github.com/WordPress/gutenberg/pull/63211)) +- Include a "Select All" options to activate/deactivate all fonts. ([63531](https://github.com/WordPress/gutenberg/pull/63531)) +- Reduce duplication of font library group headings. ([63532](https://github.com/WordPress/gutenberg/pull/63532)) + +#### Zoom Out +- Hide vertical toolbar when block is not full width. ([63650](https://github.com/WordPress/gutenberg/pull/63650)) +- Only show the inserters when a block is selected or hovered. ([63668](https://github.com/WordPress/gutenberg/pull/63668)) + +#### Block Locking +- Tweak Block Locking UI. ([63881](https://github.com/WordPress/gutenberg/pull/63881)) + +#### General UI +- Polish "Delete" modal. ([63392](https://github.com/WordPress/gutenberg/pull/63392)) +- Update close icon. ([63597](https://github.com/WordPress/gutenberg/pull/63597)) +- Site Editor: Reduce navigation sidebar width. ([63431](https://github.com/WordPress/gutenberg/pull/63431)) + +#### Block bindings +- Bootstrap sources defined in the server. ([63470](https://github.com/WordPress/gutenberg/pull/63470)) + +#### Patterns +- Limit pattern shuffling to theme and user patterns only. ([62677](https://github.com/WordPress/gutenberg/pull/62677)) + +#### Components +- CustomSelectControl V2 legacy adapter: Stabilize experimental props. ([63248](https://github.com/WordPress/gutenberg/pull/63248)) +- CustomSelectControl: Switch to ariakit-based implementation. ([63258](https://github.com/WordPress/gutenberg/pull/63258)) +- CustomSelectControlV2: Animate select popover appearance. ([63343](https://github.com/WordPress/gutenberg/pull/63343)) +- CustomSelectControlV2: Do not flip popover if legacy adapter. ([63357](https://github.com/WordPress/gutenberg/pull/63357)) +- DropdownMenuV2: Invert animation direction. ([63443](https://github.com/WordPress/gutenberg/pull/63443)) +- FontSizePicker: Tidy up internal logic. ([63553](https://github.com/WordPress/gutenberg/pull/63553)) +- FormTokenField: Deprecate bottom margin. ([63491](https://github.com/WordPress/gutenberg/pull/63491)) +- SelectControl: Add "minimal" variant. ([63265](https://github.com/WordPress/gutenberg/pull/63265)) +- Tabs: Hyphenate tab labels. ([63337](https://github.com/WordPress/gutenberg/pull/63337)) +- Tabs: Keep full opacity of focus ring on disabled tabs. ([63754](https://github.com/WordPress/gutenberg/pull/63754)) +- Update HeightControl component to label inputs. ([63761](https://github.com/WordPress/gutenberg/pull/63761)) + +#### Core Data +- Core Data: Mark 'canUser' related actions resolvers as resolved. ([63435](https://github.com/WordPress/gutenberg/pull/63435)) +- Core Data: Resolve user capabilities when fetching an entity. ([63430](https://github.com/WordPress/gutenberg/pull/63430)) +- Core Data: Support entities in the 'canUser' selector. ([63322](https://github.com/WordPress/gutenberg/pull/63322)) +- Core Data: Support entity queries in the 'useResourcePermissions' hook. ([63653](https://github.com/WordPress/gutenberg/pull/63653)) + +#### JSON Schemas +- Update JSON Schemas to Draft 7. ([63583](https://github.com/WordPress/gutenberg/pull/63583)) + +### New APIs + +#### Block bindings +- Unify `getValue`/`getValues` and `setValue`/`setValues` APIs. ([63185](https://github.com/WordPress/gutenberg/pull/63185)) + +### Bug Fixes + +#### Data Views +- DataViews: Do not render bulk actions Dropdown if no actions are available. ([63575](https://github.com/WordPress/gutenberg/pull/63575)) +- DataViews: Fix default layouts in the pages data views. ([63427](https://github.com/WordPress/gutenberg/pull/63427)) +- DataViews: Fix featured image height regression. ([63424](https://github.com/WordPress/gutenberg/pull/63424)) +- DataViews: Fix field rendering. ([63452](https://github.com/WordPress/gutenberg/pull/63452)) +- DataViews: Fix pattens list selection. ([63733](https://github.com/WordPress/gutenberg/pull/63733)) +- DataViews: Fix uncontrolled selection. ([63741](https://github.com/WordPress/gutenberg/pull/63741)) +- DataViews: Only show elligible actions in the bulk editing menu. ([63473](https://github.com/WordPress/gutenberg/pull/63473)) +- Fix patterns sorting by `title`. ([63710](https://github.com/WordPress/gutenberg/pull/63710)) +- Fix selected row styles in table layout. ([63811](https://github.com/WordPress/gutenberg/pull/63811)) +- Fix: DataViews: Layout resets for patterns each time a new pattern category is selected. ([63711](https://github.com/WordPress/gutenberg/pull/63711)) +- Fix: Inconsistent field spacing in Grid layout. ([63363](https://github.com/WordPress/gutenberg/pull/63363)) +- Templates DataViews: Set the right context for the preview field. ([63488](https://github.com/WordPress/gutenberg/pull/63488)) +- +#### Block Editor +- Fix user patterns disabling sync filter. ([63828](https://github.com/WordPress/gutenberg/pull/63828)) +- ImageURLInputUI: Make onSetLightbox and resetLightbox optional. ([63573](https://github.com/WordPress/gutenberg/pull/63573)) +- Pattern Inserter: Fix pagination layout when "Show button text labels" enabled. ([63466](https://github.com/WordPress/gutenberg/pull/63466)) +- Patterns inserter tabs: Temporary disable animated indicator. ([63352](https://github.com/WordPress/gutenberg/pull/63352)) +- Prevent empty void at the bottom of editor when block directory results are present. ([63397](https://github.com/WordPress/gutenberg/pull/63397)) +- Remove double shadow on Inserter category panel when zoomed out. ([63516](https://github.com/WordPress/gutenberg/pull/63516)) +- Tabs: Vertical Tabs should be 40px min height. ([63446](https://github.com/WordPress/gutenberg/pull/63446)) +- Fix mobile styles for inserter pattern and media tab navigation. ([63451](https://github.com/WordPress/gutenberg/pull/63451)) +- useBlockElement: Return null until ref callback has time to clean up the old element. ([63565](https://github.com/WordPress/gutenberg/pull/63565)) +- Remove hint in the Settings tab. ([63515](https://github.com/WordPress/gutenberg/pull/63515)) + +#### Block Library +- Avoid stripping attributes via group block migration when no layout is specified. ([63837](https://github.com/WordPress/gutenberg/pull/63837)) +- Fix default unit issue for tag cloud block. ([59122](https://github.com/WordPress/gutenberg/pull/59122)) +- Footnotes: Register format within the init function. ([63554](https://github.com/WordPress/gutenberg/pull/63554)) +- Image lightbox: Remove duplicate image when lightbox is opened. ([63381](https://github.com/WordPress/gutenberg/pull/63381)) +- Query Loop: Fix 'block' scoped variations to get the `query` defaults. ([63477](https://github.com/WordPress/gutenberg/pull/63477)) +- Query Loop: Fix passing of `namespace` when selecting from suggested patterns. ([63402](https://github.com/WordPress/gutenberg/pull/63402)) +- Template Part: Add check if create action should be allowed. ([63623](https://github.com/WordPress/gutenberg/pull/63623)) +- Update Inherited Query Loop value from Template Settings changes. ([63358](https://github.com/WordPress/gutenberg/pull/63358)) + +#### Site Editor +- Fix: Error while Calling edit-site getCurrentTemplateTemplateParts selector. ([63818](https://github.com/WordPress/gutenberg/pull/63818)) +- Fix error when duplicating a template part. ([63663](https://github.com/WordPress/gutenberg/pull/63663)) +- Fix: Add Template Modal layout in mobile view. ([63627](https://github.com/WordPress/gutenberg/pull/63627)) +- Make hover block outlines not present in Distraction Free. ([63819](https://github.com/WordPress/gutenberg/pull/63819)) +- Site Editor Navigation Commands: Add permission check. ([63798](https://github.com/WordPress/gutenberg/pull/63798)) +- fix: Wp icon focus issue. ([62675](https://github.com/WordPress/gutenberg/pull/62675)) + +#### Zoom Out +- Don't automatically show inserter when zoom out mode initiates. ([63859](https://github.com/WordPress/gutenberg/pull/63859)) +- Ensure that we only enter zoom out mode if the experiment is enabled. ([63417](https://github.com/WordPress/gutenberg/pull/63417)) +- Fix crash due to absence of selected block. ([63642](https://github.com/WordPress/gutenberg/pull/63642)) +- Fix vertical toolbar position. ([63745](https://github.com/WordPress/gutenberg/pull/63745)) +- Translate toolbar delete button. ([63476](https://github.com/WordPress/gutenberg/pull/63476)) + +#### Components +- Button: Never apply `aria-disabled` to anchor. ([63376](https://github.com/WordPress/gutenberg/pull/63376)) +- Revert "Update HeightControl component to label inputs". ([63839](https://github.com/WordPress/gutenberg/pull/63839)) +- SelectControl: Fix hover/focus color in wp-admin. ([63855](https://github.com/WordPress/gutenberg/pull/63855)) +- ToggleGroupControl: Support `disabled` options. ([63450](https://github.com/WordPress/gutenberg/pull/63450)) + +#### Global Styles +- Disable "Reset styles" button when there are no changes. ([63562](https://github.com/WordPress/gutenberg/pull/63562)) +- Disallow scrolling the block preview. ([63558](https://github.com/WordPress/gutenberg/pull/63558)) +- Ensure root selector (body) is not wrapped in :root :Where(). ([63726](https://github.com/WordPress/gutenberg/pull/63726)) +- Global styles block previews: Fix scaling. ([63596](https://github.com/WordPress/gutenberg/pull/63596)) +- Style variations: Don't display the default if its the only variation. ([63555](https://github.com/WordPress/gutenberg/pull/63555)) + +#### CSS & Styling +- Comments: Allow button element shadows from theme.json. ([63790](https://github.com/WordPress/gutenberg/pull/63790)) +- List: Prevent style bleed into non-List block lists. ([63537](https://github.com/WordPress/gutenberg/pull/63537)) +- Search: Prevent override of global button radii in editor. ([63789](https://github.com/WordPress/gutenberg/pull/63789)) + +#### Font Library +- Add 'No fonts installed' message on library tab when fonts aren't available. ([63740](https://github.com/WordPress/gutenberg/pull/63740)) +- Improve 'No fonts installed' state when fonts are installed but not activated. ([63533](https://github.com/WordPress/gutenberg/pull/63533)) + +#### Post Editor +- Allow editing of description only for custom templates. ([63664](https://github.com/WordPress/gutenberg/pull/63664)) + +#### Design Tools +- Background image block support: Fix dropzone size. ([63588](https://github.com/WordPress/gutenberg/pull/63588)) +- Background tool: Fix double border. ([63559](https://github.com/WordPress/gutenberg/pull/63559)) + +#### General interface +- Discussions panel: Distinguish between verb and adjective form of open for internationalization. ([63791](https://github.com/WordPress/gutenberg/pull/63791)) +- Fix canvas issues by removing VisualEditor’s height. ([63724](https://github.com/WordPress/gutenberg/pull/63724)) + +#### Block Transforms +- Block Switcher Preview: Adjust the position and enable pattern list preview in mobile viewport. ([63512](https://github.com/WordPress/gutenberg/pull/63512)) + +#### Block bindings +- Revert triggering multi-entity save panel in post with meta changes. ([63412](https://github.com/WordPress/gutenberg/pull/63412)) + +#### Block Directory +- Memoize store selectors. ([63346](https://github.com/WordPress/gutenberg/pull/63346)) + +#### Inner blocks +- InnerBlocks: Make sure blockType is set before trying to use it. ([63351](https://github.com/WordPress/gutenberg/pull/63351)) + +#### Widgets Editor +- Widgets: Memoize 'getWidgets' store selector. ([63338](https://github.com/WordPress/gutenberg/pull/63338)) + +#### Synced Patterns +- Pattern overrides: Ensure "Reset" button always shows as last item and with border. ([63291](https://github.com/WordPress/gutenberg/pull/63291)) + +#### Patterns +- Fix: Removed shuffle button when only 1 pattern is present. ([63093](https://github.com/WordPress/gutenberg/pull/63093)) + +#### Media +- Lock post saving during image uploads. ([41120](https://github.com/WordPress/gutenberg/pull/41120)) + +#### JSON Schemas +- Prepare JSON schemas for Draft 7 update. ([63582](https://github.com/WordPress/gutenberg/pull/63582)) + +#### Security +- Add: Permission checks to avoid 403 errors on non admin roles. ([63296](https://github.com/WordPress/gutenberg/pull/63296)) + +### Accessibility + +#### Components +- Align checkbox, radio, and toggle input design. ([63490](https://github.com/WordPress/gutenberg/pull/63490)) +- Fix ComboboxControl reset button when using the keyboard. ([63410](https://github.com/WordPress/gutenberg/pull/63410)) + +#### Post Editor +- Add missing aria-haspopup attribute to the buttons to set and replace the featured image. ([63360](https://github.com/WordPress/gutenberg/pull/63360)) + +#### Block Library +- Show visual label for Categories block in dropdown mode. ([56364](https://github.com/WordPress/gutenberg/pull/56364)) + + +### Performance + +#### Components +- Storybook: Improve TypeScript performance for slow stories. ([63388](https://github.com/WordPress/gutenberg/pull/63388)) + + +### Experiments + +#### Grid layout +- Disable in-between inserter in Manual grids. ([63391](https://github.com/WordPress/gutenberg/pull/63391)) +- Don't display default appender inside Manual grid. ([63395](https://github.com/WordPress/gutenberg/pull/63395)) +- Fix responsive behaviour so both column start and column span are taken into account. ([63464](https://github.com/WordPress/gutenberg/pull/63464)) +- Better looking block movers. ([63394](https://github.com/WordPress/gutenberg/pull/63394)) +- Place new block after currently selected block when using slash inserter and splitting text. ([63333](https://github.com/WordPress/gutenberg/pull/63333)) +- Move visualizer popover to slot under the canvas. ([63389](https://github.com/WordPress/gutenberg/pull/63389)) +- Don't remount the block when rendering grid tools. ([63557](https://github.com/WordPress/gutenberg/pull/63557)) + +#### Data Views +- Quick Edit: Support bulk selection. ([63841](https://github.com/WordPress/gutenberg/pull/63841)) +- DataViews: Bootstrap Quick Edit. ([63600](https://github.com/WordPress/gutenberg/pull/63600)) + + +### Documentation + +- Add to code requirements install and import Interactivity API. ([63439](https://github.com/WordPress/gutenberg/pull/63439)) +- Alpine vs Preact extra explanations. ([63593](https://github.com/WordPress/gutenberg/pull/63593)) +- Backport docs: Update and format. ([63830](https://github.com/WordPress/gutenberg/pull/63830)) +- Create-block - fix - update default folder name to proper default. ([63530](https://github.com/WordPress/gutenberg/pull/63530)) +- DataForm: Add a simple story for the DataForm component. ([63840](https://github.com/WordPress/gutenberg/pull/63840)) +- Fix Typo in Interactivity Api Reference. ([63775](https://github.com/WordPress/gutenberg/pull/63775)) +- Fix typo in Autocomplete component README.md. ([63496](https://github.com/WordPress/gutenberg/pull/63496)) +- FontSizePicker: Fix documentation for default `units`. ([63577](https://github.com/WordPress/gutenberg/pull/63577)) +- Improve the base control help prop documentation. ([63693](https://github.com/WordPress/gutenberg/pull/63693)) +- JSON Schema Docgen Rework. ([63868](https://github.com/WordPress/gutenberg/pull/63868)) +- Mark unstable__bootstrapServerSideBlockDefinitions with @ignore. ([63673](https://github.com/WordPress/gutenberg/pull/63673)) +- Move entity-provider.js exports into hooks/index.ts so they are added to the documentation. ([63528](https://github.com/WordPress/gutenberg/pull/63528)) +- Small Typo in Experiment Page. ([63773](https://github.com/WordPress/gutenberg/pull/63773)) +- Storybook: Remove popover-related height buffers. ([63480](https://github.com/WordPress/gutenberg/pull/63480)) +- Update "Versions in WordPress" page. ([63869](https://github.com/WordPress/gutenberg/pull/63869)) +- Update dataviews documentation. ([63860](https://github.com/WordPress/gutenberg/pull/63860)) +- Update getContext() usage examples with namespace argument. ([63411](https://github.com/WordPress/gutenberg/pull/63411)) +- Update react reference links in developer documentation. ([62818](https://github.com/WordPress/gutenberg/pull/62818)) +- Update react reference links in package's readme and doc blocks. ([62704](https://github.com/WordPress/gutenberg/pull/62704)) +- Updated Useeffect URL. ([63494](https://github.com/WordPress/gutenberg/pull/63494)) + + +### Code Quality + +- Add margin-bottom lint rules for CheckboxControl, ComboboxControl, SearchControl. ([63679](https://github.com/WordPress/gutenberg/pull/63679)) +- Add margin-bottom lint rules for FocalPointPicker, TextareaControl, TreeSelect. ([63633](https://github.com/WordPress/gutenberg/pull/63633)) +- Add margin-bottom lint rules for RangeControl. ([63821](https://github.com/WordPress/gutenberg/pull/63821)) +- Block editor settings: Add missing global styles links dependencies. ([63823](https://github.com/WordPress/gutenberg/pull/63823)) +- Core Data: Remove leftover 'todo' comment. ([63842](https://github.com/WordPress/gutenberg/pull/63842)) +- Core Data: Use meta-store actions for resolution status. ([63469](https://github.com/WordPress/gutenberg/pull/63469)) +- core-data: Fix `canUser` allowed methods handling. ([63615](https://github.com/WordPress/gutenberg/pull/63615)) +- DataViews: Move PostList component to its own folder. ([63334](https://github.com/WordPress/gutenberg/pull/63334)) +- JSON Schema Reorganization and Fixes. ([63591](https://github.com/WordPress/gutenberg/pull/63591)) +- Update: Simplify and do not pass renderingMode on editor SidebarContent. ([63814](https://github.com/WordPress/gutenberg/pull/63814)) +- Use Base Focus Styles for Region Focus. ([62881](https://github.com/WordPress/gutenberg/pull/62881)) +- Use static 'key' when filtering BlockEdit components. ([63590](https://github.com/WordPress/gutenberg/pull/63590)) +- Update: Simplify some permission checks. ([63812](https://github.com/WordPress/gutenberg/pull/63812)) +- Use entity details when calling 'canUser' selectors. ([63415](https://github.com/WordPress/gutenberg/pull/63415)) +- HTML API: Backport updates from Core. ([63723](https://github.com/WordPress/gutenberg/pull/63723)) + +#### Block Library +- Image block: Remove unnecessary variables on expand on click implementation. ([63290](https://github.com/WordPress/gutenberg/pull/63290)) +- Image lightbox: Move image data from context to state. ([63348](https://github.com/WordPress/gutenberg/pull/63348)) +- Navigation Submenu: Remove user permission checks. ([63720](https://github.com/WordPress/gutenberg/pull/63720)) +- Query Title block: Rely on the editor store to apply the right archive title placeholder. ([63478](https://github.com/WordPress/gutenberg/pull/63478)) +- Remove unused useSplit after #54543. ([63826](https://github.com/WordPress/gutenberg/pull/63826)) + +#### Data Views +- DataViews: Cleanup preview styles. ([63365](https://github.com/WordPress/gutenberg/pull/63365)) +- DataViews: Move the layouts into a dedicated folder. ([63409](https://github.com/WordPress/gutenberg/pull/63409)) +- DataViews: Refactor to prepare exposing the underlying UI pieces. ([63694](https://github.com/WordPress/gutenberg/pull/63694)) +- DataViews: Remove redundant setSelection prop. ([63648](https://github.com/WordPress/gutenberg/pull/63648)) +- DataViews: Rename `onSelectionChange` to `onChangeSelection`. ([63087](https://github.com/WordPress/gutenberg/pull/63087)) + +#### Components +- ColorPicker: Use `minimal` variant for SelectControl. ([63676](https://github.com/WordPress/gutenberg/pull/63676)) +- Rename Button describedBy prop to description and deprecate old name. ([63486](https://github.com/WordPress/gutenberg/pull/63486)) +- Tabs: Move animation-related utilities into separate utils file. ([62946](https://github.com/WordPress/gutenberg/pull/62946)) + +#### Block bindings +- Don't provide default `canUserEditValue` in reducer. ([63628](https://github.com/WordPress/gutenberg/pull/63628)) +- Improve how the context needed by sources is extended in the editor. ([63513](https://github.com/WordPress/gutenberg/pull/63513)) +- Improve the way block bindings sources are registered. ([63117](https://github.com/WordPress/gutenberg/pull/63117)) + +#### Post Editor +- Editor: Remove unused `setNestedValue` util. ([63620](https://github.com/WordPress/gutenberg/pull/63620)) +- Move useSelectNearestEditableBlock out of src/hooks. ([63730](https://github.com/WordPress/gutenberg/pull/63730)) + +#### Font Library +- Remove unused font library experiment. ([63890](https://github.com/WordPress/gutenberg/pull/63890)) + +#### Global Styles +- Remove unused global styles background screen. ([63887](https://github.com/WordPress/gutenberg/pull/63887)) + +#### Widgets Editor +- Widget Editor: Remove unused values returned from 'mapSelect'. ([63738](https://github.com/WordPress/gutenberg/pull/63738)) + +#### Block API +- Use `@wordpress/warning` during block registration instead of `console.error` and `console.warn`. ([63610](https://github.com/WordPress/gutenberg/pull/63610)) + +#### Synced Patterns +- Quality: Remove "reusable block name hint" code. ([63514](https://github.com/WordPress/gutenberg/pull/63514)) + +#### Commands +- Update cmdk. ([63465](https://github.com/WordPress/gutenberg/pull/63465)) + +#### Document Settings +- FlatTermSelector: Be more defensive about termIds. ([63461](https://github.com/WordPress/gutenberg/pull/63461)) + +#### Site Editor +- Deprecate 'getCanUserCreateMedia' selector. ([63413](https://github.com/WordPress/gutenberg/pull/63413)) + +#### Block Directory +- Remove 'edit-post' package dependency. ([63349](https://github.com/WordPress/gutenberg/pull/63349)) + +### Tools + +#### Project Management +- Issue template: Use checkboxes instead of dropdown. ([63523](https://github.com/WordPress/gutenberg/pull/63523)) +- Sync backport changelog action: Use outputs instead of env. ([63792](https://github.com/WordPress/gutenberg/pull/63792)) +- Run sync when issue is labeled with Sync Backport Changelog. ([63793](https://github.com/WordPress/gutenberg/pull/63793)) + +#### Testing +- Downgrade node 22(.5) unit tests to 22.4. ([63728](https://github.com/WordPress/gutenberg/pull/63728)) +- Font Library: Fix flaky end-to-end tests. ([63904](https://github.com/WordPress/gutenberg/pull/63904)) +- Upgrade Playwright to v1.45. ([61443](https://github.com/WordPress/gutenberg/pull/61443)) +- Bug: Eslint `recommended-with-formatting` allows for unnecessary spaces. ([63549](https://github.com/WordPress/gutenberg/pull/63549)) + +#### Build Tooling & Plugin +- Fix broken license check script. ([61868](https://github.com/WordPress/gutenberg/pull/61868)) +- React: Restore umd builds. ([63602](https://github.com/WordPress/gutenberg/pull/63602)) +- Upgrade TypeScript to 5.5. ([63012](https://github.com/WordPress/gutenberg/pull/63012)) +- Scripts: Remove now-obsolete `getRenderPropPaths()`. ([63661](https://github.com/WordPress/gutenberg/pull/63661)) +- Scripts: Include variations paths in build. ([63098](https://github.com/WordPress/gutenberg/pull/63098)) + + +## First-time contributors + +The following PRs were merged by first-time contributors: + +- @hectorjarquin: Add to code requirements install and import Interactivity API. ([63439](https://github.com/WordPress/gutenberg/pull/63439)) +- @Sourav61: Fix: Removed shuffle button when only 1 pattern is present. ([63093](https://github.com/WordPress/gutenberg/pull/63093)) +- @tomllobet: Create-block - fix - update default folder name to proper default. ([63530](https://github.com/WordPress/gutenberg/pull/63530)) +- @troychaplin: change: Updated soundcloud icon for social link block. ([63504](https://github.com/WordPress/gutenberg/pull/63504)) + + +## Contributors + +The following contributors merged PRs in this release: + +@aaronrobertshaw @adamsilverstein @afercia @ajlende @akasunil @amitraj2203 @andrewserong @artemiomorales @barryceelen @carolinan @ciampo @DaniGuardiola @dhananjaykuber @dmsnell @dsas @ellatrix @geriux @hectorjarquin @jameskoster @jasmussen @jeherve @jeryj @jorgefilipecosta @jsnajdr @juanmaguitar @kevin940726 @luisherranz @madhusudhand @MaggieCabrera @Mamaduka @matiasbenedetto @mattsherman @mirka @noisysocks @ntsekouras @oandregal @ockham @priethor @ramonjd @richtabor @ryanwelcher @SantosGuillamot @scruffian @shail-mehta @sirreal @Sourav61 @stokesman @StyleShit @swissspidy @t-hamano @talldan @tellthemachines @tomllobet @troychaplin @tyxla @up1512001 @widoz @youknowriad + + = 18.8.0 = ## Changelog diff --git a/docs/assets/plugin-sidebar-closed-state.png b/docs/assets/plugin-sidebar-closed-state.png deleted file mode 100644 index 025da900ffcdd594c27a097e0b6ca23928377ec1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6773 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 GPyY`qtQ;l) literal 10854 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 D)n!^F diff --git a/docs/contributors/code/getting-started-with-code-contribution.md b/docs/contributors/code/getting-started-with-code-contribution.md index 921c8ad6ddc3e..df6b305f35983 100644 --- a/docs/contributors/code/getting-started-with-code-contribution.md +++ b/docs/contributors/code/getting-started-with-code-contribution.md @@ -16,7 +16,7 @@ We recommend using the [Node Version Manager](https://github.com/nvm-sh/nvm) (nv We recommend using the [wp-env package](/packages/env/README.md) for setting WordPress environment locally. You'll need to install Docker to use `wp-env`. See the [Development Environment tutorial for additional details](/docs/getting-started/devenv/README.md). > Note: To install Docker on Windows 10 Home Edition, follow the [install instructions from Docker for Windows with WSL2](https://docs.docker.com/docker-for-windows/wsl/). -As an alternative to Docker setup, you can use [Local](https://localwp.com/), [WampServer](http://www.wampserver.com/en/), or [MAMP](https://www.mamp.info/), or even use a remote server. +As an alternative to Docker setup, you can use [Local](https://localwp.com/), [WampServer](https://wampserver.aviatechno.net/), or [MAMP](https://www.mamp.info/), or even use a remote server. - GitHub CLI Although not a requirement, the [GitHub CLI](https://cli.github.com/) can be very useful in helping you checkout pull requests locally. Both from the Gutenberg repo and forked repos. This can be a major time saver while code reviewing and testing pull requests. @@ -134,7 +134,7 @@ If you run into an issue, check the [troubleshooting section in `wp-env` documen ### Using Local or MAMP -As an alternative to Docker and `wp-env`, you can also use [Local](https://localwp.com/), [WampServer](http://www.wampserver.com/en/), or [MAMP](https://www.mamp.info/) to run a local WordPress environment. To do so clone and install Gutenberg as a regular plugin in your installation by creating a symlink or copying the directory to the proper `wp-content/plugins` directory. +As an alternative to Docker and `wp-env`, you can also use [Local](https://localwp.com/), [WampServer](https://wampserver.aviatechno.net/), or [MAMP](https://www.mamp.info/) to run a local WordPress environment. To do so clone and install Gutenberg as a regular plugin in your installation by creating a symlink or copying the directory to the proper `wp-content/plugins` directory. You will also need some extra configuration to be able to run the e2e tests. diff --git a/docs/getting-started/devenv/README.md b/docs/getting-started/devenv/README.md index 1c2b9ed9070d3..4539dacbdf504 100644 --- a/docs/getting-started/devenv/README.md +++ b/docs/getting-started/devenv/README.md @@ -54,6 +54,7 @@ Refer to the [Get started with `wp-env`](/docs/getting-started/devenv/get-starte This list is not exhaustive, but here are several additional options to choose from if you prefer not to use `wp-env`: - [Local](https://localwp.com/) +- [WP Studio](https://developer.wordpress.com/studio/) - [XAMPP](https://www.apachefriends.org/) - [MAMP](https://www.mamp.info/en/mamp/mac/) - [Varying Vagrant Vagrants](https://varyingvagrantvagrants.org/) (VVV) diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 3309a676dfc84..5beb712c80a11 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -237,7 +237,7 @@ Displays a title with the number of comments. ([Source](https://github.com/WordP - **Category:** theme - **Ancestor:** core/comments - **Supports:** align, color (background, gradients, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~anchor~~, ~~html~~ -- **Attributes:** level, showCommentsCount, showPostTitle, textAlign +- **Attributes:** level, levelOptions, showCommentsCount, showPostTitle, textAlign ## Cover @@ -360,7 +360,7 @@ Introduce new sections and organize content to help visitors (and search engines - **Name:** core/heading - **Category:** text - **Supports:** __unstablePasteTextInline, align (full, wide), anchor, className, color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), splitting, typography (fontSize, lineHeight) -- **Attributes:** content, level, placeholder, textAlign +- **Attributes:** content, level, levelOptions, placeholder, textAlign ## Home Link @@ -410,7 +410,7 @@ Display a list of your most recent posts. ([Source](https://github.com/WordPress ## List -Create a bulleted or numbered list. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/list)) +An organized collection of items displayed in a specific order. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/list)) - **Name:** core/list - **Category:** text @@ -420,13 +420,13 @@ Create a bulleted or numbered list. ([Source](https://github.com/WordPress/guten ## List item -Create a list item. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/list-item)) +An individual item within a list. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/list-item)) - **Name:** core/list-item - **Category:** text - **Parent:** core/list - **Allowed Blocks:** core/list -- **Supports:** color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), splitting, typography (fontSize, lineHeight), ~~className~~ +- **Supports:** anchor, color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), splitting, typography (fontSize, lineHeight), ~~className~~ - **Attributes:** content, placeholder ## Login/out @@ -435,7 +435,7 @@ Show login & logout links. ([Source](https://github.com/WordPress/gutenberg/tree - **Name:** core/loginout - **Category:** theme -- **Supports:** className, interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight) +- **Supports:** className, color (background, gradients, link, ~~text~~), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight) - **Attributes:** displayLoginAsForm, redirectToCurrent ## Media & Text @@ -689,7 +689,7 @@ Displays the title of a post, page, or any other content-type. ([Source](https:/ - **Name:** core/post-title - **Category:** theme - **Supports:** align (full, wide), color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ -- **Attributes:** isLink, level, linkTarget, rel, textAlign +- **Attributes:** isLink, level, levelOptions, linkTarget, rel, textAlign ## Preformatted @@ -775,7 +775,7 @@ Display the query title. ([Source](https://github.com/WordPress/gutenberg/tree/t - **Name:** core/query-title - **Category:** theme - **Supports:** align (full, wide), color (background, gradients, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ -- **Attributes:** level, showPrefix, showSearchTerm, textAlign, type +- **Attributes:** level, levelOptions, showPrefix, showSearchTerm, textAlign, type ## Quote @@ -847,7 +847,7 @@ Describe in a few words what the site is about. The tagline can be used in searc - **Name:** core/site-tagline - **Category:** theme - **Supports:** align (full, wide), color (background, gradients, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ -- **Attributes:** level, textAlign +- **Attributes:** level, levelOptions, textAlign ## Site Title @@ -856,7 +856,7 @@ Displays the name of this site. Update the block, and the changes apply everywhe - **Name:** core/site-title - **Category:** theme - **Supports:** align (full, wide), color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ -- **Attributes:** isLink, level, linkTarget, textAlign +- **Attributes:** isLink, level, levelOptions, linkTarget, textAlign ## Social Icon @@ -908,7 +908,7 @@ Summarize your post with a list of headings. Add HTML anchors to Heading blocks ## Tag Cloud -A cloud of your most used tags. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/tag-cloud)) +A cloud of popular keywords, each sized by how often it appears. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/tag-cloud)) - **Name:** core/tag-cloud - **Category:** widgets diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 7eed5c8741288..040a10f8f506c 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -412,7 +412,7 @@ Returns all blocks that match a blockName. Results include nested blocks. _Parameters_ - _state_ `Object`: Global application state. -- _blockName_ `?string`: Optional block name, if not specified, returns an empty array. +- _blockName_ `string[]`: Block name(s) for which clientIds are to be returned. _Returns_ diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index 0ddd3858c9760..f08fbc960b8b2 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -2,7 +2,106 @@ Namespace: `core`. -## Selectors +## Dynamically generated selectors + +There are a number of user-friendly selectors that are wrappers of the more generic `getEntityRecord` and `getEntityRecords` that can be used to retrieve information for the various entities. + +### getPostType + +Returns the information for a given post type. + +_Usage_ + + import { useSelect } from '@wordpress/data'; + import { store as coreDataStore } from '@wordpress/core-data'; + + const postType = useSelect( + ( select ) => select( coreDataStore ).getPostType( 'post' ) + + // Equivalent to: select( coreDataStore ).getEntityRecord( 'root', 'postType', 'post' ) + ); + +_Parameters_ + +- postType `string` + +_Returns_ + +- `EntityRecord | undefined`: Record. + +### getPostTypes + +Returns the information for post types. + +_Usage_ + + import { useSelect } from '@wordpress/data'; + import { store as coreDataStore } from '@wordpress/core-data'; + + const postTypes = useSelect( ( select ) => { + return select( coreDataStore ).getPostTypes( { per_page: 4 } ); + + // Equivalent to: + // select( coreDataStore ).getEntityRecords( 'root', 'postType', { per_page: 4 } ); + } ); + +_Parameters_ + +- _query_ `GetRecordsHttpQuery`: Optional terms query. If requesting specific fields, fields must always include the ID. For valid query parameters see the [Reference](https://developer.wordpress.org/rest-api/reference/) in the REST API Handbook and select the entity kind. Then see the arguments available for "List [Entity kind]s". + +_Returns_ + +- `EntityRecord[] | null`: Records. + +### getTaxonomy + +Returns information for a given taxonomy. + +_Usage_ + + import { useSelect } from '@wordpress/data'; + import { store as coreDataStore } from '@wordpress/core-data'; + + const taxonomy = useSelect( ( select ) => { + return select( coreDataStore ).getTaxonomy( 'category' ); + + // Equivalent to: + // select( coreDataStore ).getEntityRecord( 'root', 'taxonomy', 'category' ); + } ); + +_Parameters_ + +- taxonomy `string` + +_Returns_ + +- `EntityRecord | undefined`: Record. + +### getTaxonomies + +Returns information for taxonomies. + +_Usage_ + + import { useSelect } from '@wordpress/data'; + import { store as coreDataStore } from '@wordpress/core-data'; + + const taxonomies = useSelect( ( select ) => { + return select( coreDataStore ).getTaxonomies( { type: 'post' } ); + + // Equivalent to: + // select( coreDataStore ).getEntityRecords( 'root', 'taxonomy', { type: 'post' } ); + } ); + +_Parameters_ + +- _query_ `GetRecordsHttpQuery`: Optional terms query. If requesting specific fields, fields must always include the ID. For valid query parameters see the [Reference](https://developer.wordpress.org/rest-api/reference/) in the REST API Handbook and select the entity kind. Then see the arguments available for "List [Entity kind]s". + +_Returns_ + +- `EntityRecord[] | null`: Records. + +## Other Selectors @@ -755,7 +854,7 @@ _Parameters_ ### receiveThemeSupports -> **Deprecated** since WP 5.9, this is not useful anymore, use the selector direclty. +> **Deprecated** since WP 5.9, this is not useful anymore, use the selector directly. Returns an action object used in signalling that the index has been received. diff --git a/docs/reference-guides/filters/block-filters.md b/docs/reference-guides/filters/block-filters.md index ee100431eb512..e7a31c1e3bbc8 100644 --- a/docs/reference-guides/filters/block-filters.md +++ b/docs/reference-guides/filters/block-filters.md @@ -4,15 +4,15 @@ WordPress exposes several APIs that allow you to modify the behavior of existing ## Registration -The following filters are available to extend block settings during their registration. +Blocks in WordPress are typically registered on both the server and client side using `block.json`` metadata. You can use the following filters to modify or extend block settings during their registration on the server with PHP and on the client with JavaScript. To learn more, refer to the [block registration](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block/) guide. ### `block_type_metadata` Filters the raw metadata loaded from the `block.json` file when registering a block type on the server with PHP. It allows modifications to be applied before the metadata gets processed. -The filter takes one parameter: +The callback function for this filter receives one parameter: -- `$metadata` (`array`) – metadata loaded from `block.json` for registering a block type. +- `$metadata` (`array`): Metadata loaded from `block.json` for registering a block type. The following example sets the `apiVersion` of all blocks to `2`. @@ -28,33 +28,33 @@ Here's a more robust example that disables background color and gradient support ```php function example_disable_heading_background_color_and_gradients( $metadata ) { - - // Only apply the filter to Heading blocks. - if ( ! isset( $metadata['name'] ) || 'core/heading' !== $metadata['name'] ) { - return $metadata; - } - - // Check if 'supports' key exists. - if ( isset( $metadata['supports'] ) && isset( $metadata['supports']['color'] ) ) { - - // Remove Background color and Gradients support. - $metadata['supports']['color']['background'] = false; - $metadata['supports']['color']['gradients'] = false; - } - - return $metadata; + + // Only apply the filter to Heading blocks. + if ( ! isset( $metadata['name'] ) || 'core/heading' !== $metadata['name'] ) { + return $metadata; + } + + // Check if 'supports' key exists. + if ( isset( $metadata['supports'] ) && isset( $metadata['supports']['color'] ) ) { + + // Remove Background color and Gradients support. + $metadata['supports']['color']['background'] = false; + $metadata['supports']['color']['gradients'] = false; + } + + return $metadata; } add_filter( 'block_type_metadata', 'example_disable_heading_background_color_and_gradients' ); ``` ### `block_type_metadata_settings` -Filters the settings determined from the processed block type metadata. It makes it possible to apply custom modifications using the block metadata that isn’t handled by default. +Filters the settings determined from the processed block type metadata. It makes it possible to apply custom modifications using the block metadata that isn't handled by default. -The filter takes two parameters: +The callback function for this filter receives two parameters: -- `$settings` (`array`) – Array of determined settings for registering a block type. -- `$metadata` (`array`) – Metadata loaded from the `block.json` file. +- `$settings` (`array`): Array of determined settings for registering a block type. +- `$metadata` (`array`): Metadata loaded from the `block.json` file. The following example increases the `apiVersion` for all blocks by `1`. @@ -66,6 +66,45 @@ function example_filter_metadata_registration( $settings, $metadata ) { add_filter( 'block_type_metadata_settings', 'example_filter_metadata_registration', 10, 2 ); ``` +### `register_block_type_args` + +Filters a block's arguments array (`$args`) right before the block type is officially registered on the server. + +The callback function for this filter receives two parameters: + +- `$args` (`array`): Array of arguments for registering a block type. +- `$block_type` (`string`): Block type name including namespace. + +`register_block_type_args` is the most low-level PHP filter available, and it will work for every block registered on the server. All settings defined on the server are propagated to the client with higher priority than those set in the client. + +The following code will disable the color controls for Paragraph, Heading, List, and List Item blocks. + +```php +function example_disable_color_for_specific_blocks( $args, $block_type ) { + + // List of block types to modify. + $block_types_to_modify = [ + 'core/paragraph', + 'core/heading', + 'core/list', + 'core/list-item' + ]; + + // Check if the current block type is in the list. + if ( in_array( $block_type, $block_types_to_modify, true ) ) { + // Disable color controls. + $args['supports']['color'] = array( + 'text' => false, + 'background' => false, + 'link' => false, + ); + } + + return $args; +} +add_filter( 'register_block_type_args', 'example_disable_color_for_specific_blocks', 10, 2 ); +``` + ### `blocks.registerBlockType` Used to filter the block settings when registering the block on the client with JavaScript. It receives the block settings, the name of the registered block, and either null or the deprecated block settings (when applied to a registered deprecation) as arguments. This filter is also applied to each of a block's deprecated settings. @@ -94,31 +133,100 @@ wp.hooks.addFilter( ); ``` -## Block Editor +## Front end + +The following PHP filters are available to change the output of a block on the front end. + +### `render_block` + +Filters the font-end content of any block. This filter has no impact on the behavior of blocks in the Editor. + +The callback function for this filter receives three parameters: + +- `$block_content` (`string`): The block content. +- `block` (`array`): The full block, including name and attributes. +- `$instance` (`WP_Block`): The block instance. + +In the following example, the class `example-class` is added to all Paragraph blocks on the front end. Here the [HTML API](https://make.wordpress.org/core/2023/03/07/introducing-the-html-api-in-wordpress-6-2/) is used to easily add the class instead of relying on regex. + +```php +function example_add_custom_class_to_paragraph_block( $block_content, $block ) { + + // Check if the block is a Paragraph block. + if ( 'core/paragraph' === $block['blockName'] ) { + + // Add the custom class to the block content using the HTML API. + $processor = new WP_HTML_Tag_Processor( $block_content ); + + if ( $processor->next_tag( 'p' ) ) { + $processor->add_class( 'example-class' ); + } + + return $processor->get_updated_html(); + } + + return $block_content; +} +add_filter( 'render_block', 'example_add_custom_class_to_paragraph_block', 10, 2 ); +``` + +### `render_block_{namespace/block}` + +Filters the font-end content of the defined block. This is just a simpler form of `render_block` when you only need to modify a specific block type. + +The callback function for this filter receives three parameters: + +- `$block_content` (`string`): The block content. +- `block` (`array`): The full block, including name and attributes. +- `$instance` (`WP_Block`): The block instance. + +In the following example, the class `example-class` is added to all Paragraph blocks on the front end. Notice that compared to the `render_block` example above, you no longer need to check the block type before modifying the content. Again, the [HTML API](https://make.wordpress.org/core/2023/03/07/introducing-the-html-api-in-wordpress-6-2/) is used instead of regex. + +```php +function example_add_custom_class_to_paragraph_block( $block_content, $block ) { + + // Add the custom class to the block content using the HTML API. + $processor = new WP_HTML_Tag_Processor( $block_content ); + + if ( $processor->next_tag( 'p' ) ) { + $processor->add_class( 'example-class' ); + } + + return $processor->get_updated_html(); +} +add_filter( 'render_block_core/paragraph', 'example_add_custom_class_to_paragraph_block', 10, 2 ); +``` + +## Editor -The following filters are available to change the behavior of blocks while editing in the block editor. +The following JavaScript filters are available to change the behavior of blocks while editing in the Editor. ### `blocks.getSaveElement` -A filter that applies to the result of a block's `save` function. This filter is used to replace or extend the element, for example using `React.cloneElement` to modify the element's props or replace its children, or returning an entirely new element. +A filter that applies to the result of a block's `save` function. This filter is used to replace or extend the element, for example using `React.cloneElement` to modify the element's props, replace its children, or return an entirely new element. -The filter's callback receives an element, a block-type definition object, and the block attributes as arguments. It should return an element. +The callback function for this filter receives three parameters: -The following example wraps a Cover block in an outer container div. +- `element` (`Object`): The element to be modified and returned. +- `blockType` (`Object`): A block-type definition object. +- `attributes` (`Object`): The block's attributes. + +The following example wraps a Cover block in an outer container `div`. ```js function wrapCoverBlockInContainer( element, blockType, attributes ) { - // skip if element is undefined + + // Skip if element is undefined. if ( ! element ) { return; } - // only apply to cover blocks + // Only apply to Cover blocks. if ( blockType.name !== 'core/cover' ) { return element; } - // return the element wrapped in a div + // Return the element wrapped in a div. return
{ element }
; } @@ -131,9 +239,13 @@ wp.hooks.addFilter( ### `blocks.getSaveContent.extraProps` -A filter that applies to all blocks returning a WP Element in the `save` function. This filter is used to add extra props to the root element of the `save` function. For example: to add a className, an id, or any valid prop for this element. +A filter that applies to all blocks returning a WP Element in the `save` function. This filter is used to add extra props to the root element of the `save` function. For example, you could add a className, an id, or any valid prop for this element. + +The callback function for this filter receives three parameters: -The filter receives the current `save` element's props, a block type, and the block attributes as arguments. It should return a props object. +- `props` (`Object`): The current `save` element's props to be modified and returned. +- `blockType` (`Object`): A block-type definition object. +- `attributes` (`Object`): The block's attributes. The following example adds a red background by default to all blocks. @@ -152,7 +264,7 @@ wp.hooks.addFilter( ); ``` -_Note:_ A [block validation](/docs/reference-guides/block-api/block-edit-save.md#validation) error will occur if this filter modifies existing content the next time the post is edited. The editor verifies that the content stored in the post matches the content output by the `save()` function. +_Note:_ A [block validation](/docs/reference-guides/block-api/block-edit-save.md#validation) error will occur if this filter modifies existing content the next time the post is edited. The Editor verifies that the content stored in the post matches the content output by the `save()` function. To avoid this validation error, use `render_block` server-side to modify existing post content instead of this filter. See [render_block documentation](https://developer.wordpress.org/reference/hooks/render_block/). @@ -160,15 +272,13 @@ To avoid this validation error, use `render_block` server-side to modify existin Generated HTML classes for blocks follow the `wp-block-{name}` nomenclature. This filter allows to provide an alternative class name. -_Example:_ - ```js -// Our filter function +// Our filter function. function setBlockCustomClassName( className, blockName ) { return blockName === 'core/code' ? 'my-plugin-code' : className; } -// Adding the filter +// Adding the filter. wp.hooks.addFilter( 'blocks.getBlockDefaultClassName', 'my-plugin/set-block-custom-class-name', @@ -188,8 +298,7 @@ Called immediately after the default parsing of a block's attributes and before Used to modify the block's `edit` component. It receives the original block `BlockEdit` component and returns a new wrapped component. -_Example:_ - +The following example adds a new Inspector panel for all blocks. ```js const { createHigherOrderComponent } = wp.compose; @@ -216,13 +325,14 @@ wp.hooks.addFilter( ); ``` - Note that as this hook is run for _all blocks_, consuming it has the potential for performance regressions, particularly around block selection metrics. To mitigate this, consider whether any work you perform can be altered to run only under certain conditions. For example, suppose you are adding components that only need to render when the block is _selected_. In that case, you can use the block's "selected" state (`props.isSelected`) to conditionalize your rendering. +The following example adds a new Inspector panel for all blocks, but only when a block is selected. + ```js const withMyPluginControls = createHigherOrderComponent( ( BlockEdit ) => { return ( props ) => { @@ -244,7 +354,7 @@ const withMyPluginControls = createHigherOrderComponent( ( BlockEdit ) => { Used to modify the block's wrapper component containing the block's `edit` component and all toolbars. It receives the original `BlockListBlock` component and returns a new wrapped component. -The following example adds a unique class name. +The following example adds a unique class name to all blocks. ```js const { createHigherOrderComponent } = wp.compose; @@ -424,7 +534,7 @@ You can also display an icon with your block category by setting an `icon` attri You can also set a custom icon in SVG format. To do so, the icon should be rendered and set on the frontend, so it can make use of WordPress SVG, allowing mobile compatibility and making the icon more accessible. -To set an SVG icon for the category shown in the previous example, add the following example JavaScript code to the editor calling `wp.blocks.updateCategory` e.g: +To set an SVG icon for the category shown in the previous example, add the following example JavaScript code to the Editor calling `wp.blocks.updateCategory` e.g: ```js ( function () { diff --git a/docs/reference-guides/interactivity-api/README.md b/docs/reference-guides/interactivity-api/README.md index 9648b0118a5aa..f5d410a8439f4 100644 --- a/docs/reference-guides/interactivity-api/README.md +++ b/docs/reference-guides/interactivity-api/README.md @@ -119,7 +119,7 @@ Here you have some more resources to learn/read more about the Interactivity API - [Interactivity API Discussions](https://github.com/WordPress/gutenberg/discussions/52882), especially the [showcase](https://github.com/WordPress/gutenberg/discussions/55642#discussioncomment-9667164) discussions. - [wpmovies.dev](http://wpmovies.dev/) demo and its [wp-movies-demo](https://github.com/WordPress/wp-movies-demo) repo - Examples using the Interactivity API at [block-development-examples](https://github.com/WordPress/block-development-examples): - - [`interactivity-api-block-833d15`](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/833d15) + - [`interactivity-api-block-833d15`](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/interactivity-api-block-833d15) - [`interactivity-api-countdown-3cd73e`](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/interactivity-api-countdown-3cd73e) - [`interactivity-api-quiz-1835fa`](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/interactivity-api-quiz-1835fa) diff --git a/docs/reference-guides/interactivity-api/api-reference.md b/docs/reference-guides/interactivity-api/api-reference.md index 0003046541532..a4b400b8c0276 100644 --- a/docs/reference-guides/interactivity-api/api-reference.md +++ b/docs/reference-guides/interactivity-api/api-reference.md @@ -55,7 +55,7 @@ The `wp-interactive` directive "activates" the interactivity for the DOM element data-wp-interactive="myPlugin" data-wp-context='{ "myColor" : "red", "myBgColor": "yellow" }' > -

I'm interactive now, >and I can use directives!

+

I'm interactive now, and I can use directives!

I'm also interactive, and I can also use directives!

@@ -66,7 +66,7 @@ The `wp-interactive` directive "activates" the interactivity for the DOM element data-wp-interactive='{ "namespace": "myPlugin" }' data-wp-context='{ "myColor" : "red", "myBgColor": "yellow" }' > -

I'm interactive now, >and I can use directives!

+

I'm interactive now, and I can use directives!

I'm also interactive, and I can also use directives!

diff --git a/docs/reference-guides/interactivity-api/iapi-faq.md b/docs/reference-guides/interactivity-api/iapi-faq.md index 5e5036779baae..dea7bc1d4367b 100644 --- a/docs/reference-guides/interactivity-api/iapi-faq.md +++ b/docs/reference-guides/interactivity-api/iapi-faq.md @@ -66,7 +66,7 @@ To summarize, using the Interactivity API rather than just using React comes wit - If you use React, your interactive blocks must generate the same markup on the client as they do on the server in PHP. Using the Interactivity API, there is no such requirement as directives are added to server-rendered HTML. - The Interactivity API is PHP-friendlier. It works out of the box with WordPress hooks or other server functionalities such as internationalization. For example, with React, you can’t know which hooks are applied on the server, and their modifications would be overwritten after hydration. -- All the benefits of [using a standard](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/what-is-interactivity-api#why-a-standard). +- All the benefits of [using a standard](/docs/reference-guides/interactivity-api/iapi-about.md#why-a-standard). ## What are the benefits of Interactivity API over just using jQuery or vanilla JavaScript? diff --git a/docs/reference-guides/slotfills/plugin-sidebar.md b/docs/reference-guides/slotfills/plugin-sidebar.md index 8fe274414905b..dbf1c5643d3aa 100644 --- a/docs/reference-guides/slotfills/plugin-sidebar.md +++ b/docs/reference-guides/slotfills/plugin-sidebar.md @@ -1,30 +1,68 @@ # PluginSidebar -This slot allows for adding items into the Gutenberg Toolbar. -Using this slot will add an icon to the bar that, when clicked, will open a sidebar with the content of the items wrapped in the `` component. +This slot allows adding items to the tool bar of either the Post or Site editor screens. +Using this slot will add an icon to the toolbar that, when clicked, opens a panel with containing the items wrapped in the `` component. ## Example -```js -import { registerPlugin } from '@wordpress/plugins'; +```jsx +import { __ } from '@wordpress/i18n'; import { PluginSidebar } from '@wordpress/editor'; -import { image } from '@wordpress/icons'; +import { + PanelBody, + Button, + TextControl, + SelectControl, +} from '@wordpress/components'; +import { registerPlugin } from '@wordpress/plugins'; +import { useState } from '@wordpress/element'; + +const PluginSidebarExample = () => { + const [ text, setText ] = useState( '' ); + const [ select, setSelect ] = useState( 'a' ); -const PluginSidebarTest = () => ( - -

Plugin Sidebar

-
-); + return ( + + +

+ { __( 'This is a heading for the PluginSidebar example.' ) } +

+

+ { __( + 'This is some example text for the PluginSidebar example.' + ) } +

+ setText( newText ) } + /> + setSelect( newSelect ) } + /> + +
+
+ ); +}; -registerPlugin( 'plugin-sidebar-test', { render: PluginSidebarTest } ); +// Register the plugin. +registerPlugin( 'plugin-sidebar-example', { render: PluginSidebarExample } ); ``` ## Location -### Closed State - -![Closed State](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/plugin-sidebar-closed-state.png?raw=true) - ### Open State ![Open State](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/plugin-sidebar-open-state.png?raw=true) diff --git a/gutenberg.php b/gutenberg.php index dfe3e6dc8a428..6ec0a56f00e74 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -3,9 +3,9 @@ * Plugin Name: Gutenberg * Plugin URI: https://github.com/WordPress/gutenberg * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. - * Requires at least: 6.4 + * Requires at least: 6.5 * Requires PHP: 7.2 - * Version: 18.9.0-rc.1 + * Version: 19.0.0-rc.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/block-supports/block-style-variations.php b/lib/block-supports/block-style-variations.php index d1a63915f1288..73e4f9fce2584 100644 --- a/lib/block-supports/block-style-variations.php +++ b/lib/block-supports/block-style-variations.php @@ -191,7 +191,7 @@ function gutenberg_render_block_style_variation_support_styles( $parsed_block ) return $parsed_block; } - wp_register_style( 'block-style-variation-styles', false, array( 'global-styles', 'wp-block-library' ) ); + wp_register_style( 'block-style-variation-styles', false, array( 'wp-block-library', 'global-styles' ) ); wp_add_inline_style( 'block-style-variation-styles', $variation_styles ); /* diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index 350d158cd6e24..ddbd1917c3054 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -1019,27 +1019,10 @@ function gutenberg_restore_group_inner_container( $block_content, $block ) { $processor = new WP_HTML_Tag_Processor( $block_content ); if ( $processor->next_tag( array( 'class_name' => 'wp-block-group' ) ) ) { - if ( method_exists( $processor, 'class_list' ) ) { - foreach ( $processor->class_list() as $class_name ) { - if ( str_contains( $class_name, 'layout' ) ) { - array_push( $layout_classes, $class_name ); - $processor->remove_class( $class_name ); - } - } - } else { - /* - * The class_list method was only added in 6.4 so this needs a temporary fallback. - * This fallback should be removed when the minimum supported version is 6.4. - */ - $classes = $processor->get_attribute( 'class' ); - if ( $classes ) { - $classes = explode( ' ', $classes ); - foreach ( $classes as $class_name ) { - if ( str_contains( $class_name, 'is-layout-' ) ) { - array_push( $layout_classes, $class_name ); - $processor->remove_class( $class_name ); - } - } + foreach ( $processor->class_list() as $class_name ) { + if ( str_contains( $class_name, 'layout' ) ) { + array_push( $layout_classes, $class_name ); + $processor->remove_class( $class_name ); } } } diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 65a5e5fe4b957..ad8722091c2d4 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -1443,9 +1443,16 @@ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' protected function process_blocks_custom_css( $css, $selector ) { $processed_css = ''; + if ( empty( $css ) ) { + return $processed_css; + } + // Split CSS nested rules. $parts = explode( '&', $css ); foreach ( $parts as $part ) { + if ( empty( $part ) ) { + continue; + } $is_root_css = ( ! str_contains( $part, '{' ) ); if ( $is_root_css ) { // If the part doesn't contain braces, it applies to the root level. @@ -1458,11 +1465,24 @@ protected function process_blocks_custom_css( $css, $selector ) { } $nested_selector = $part[0]; $css_value = $part[1]; - $part_selector = str_starts_with( $nested_selector, ' ' ) + + /* + * Handle pseudo elements such as ::before, ::after etc. Regex will also + * capture any leading combinator such as >, +, or ~, as well as spaces. + * This allows pseudo elements as descendants e.g. `.parent ::before`. + */ + $matches = array(); + $has_pseudo_element = preg_match( '/([>+~\s]*::[a-zA-Z-]+)/', $nested_selector, $matches ); + $pseudo_part = $has_pseudo_element ? $matches[1] : ''; + $nested_selector = $has_pseudo_element ? str_replace( $pseudo_part, '', $nested_selector ) : $nested_selector; + + // Finalize selector and re-append pseudo element if required. + $part_selector = str_starts_with( $nested_selector, ' ' ) ? static::scope_selector( $selector, $nested_selector ) : static::append_to_selector( $selector, $nested_selector ); - $final_selector = ":root :where($part_selector)"; - $processed_css .= $final_selector . '{' . trim( $css_value ) . '}'; + $final_selector = ":root :where($part_selector)$pseudo_part"; + + $processed_css .= $final_selector . '{' . trim( $css_value ) . '}'; } } return $processed_css; @@ -2363,6 +2383,17 @@ protected static function compute_style_properties( $styles, $settings = array() // Processes background styles. if ( 'background' === $value_path[0] && isset( $styles['background'] ) ) { + /* + * For user-uploaded images at the block level, assign defaults. + * Matches defaults applied in the editor and in block supports: background.php. + */ + if ( static::ROOT_BLOCK_SELECTOR !== $selector && ! empty( $styles['background']['backgroundImage']['id'] ) ) { + $styles['background']['backgroundSize'] = $styles['background']['backgroundSize'] ?? 'cover'; + // If the background size is set to `contain` and no position is set, set the position to `center`. + if ( 'contain' === $styles['background']['backgroundSize'] && empty( $styles['background']['backgroundPosition'] ) ) { + $styles['background']['backgroundPosition'] = 'center'; + } + } $background_styles = gutenberg_style_engine_get_styles( array( 'background' => $styles['background'] ) ); $value = $background_styles['declarations'][ $css_property ] ?? $value; } diff --git a/lib/class-wp-theme-json-resolver-gutenberg.php b/lib/class-wp-theme-json-resolver-gutenberg.php index a2153e639db3b..2231cb0f11538 100644 --- a/lib/class-wp-theme-json-resolver-gutenberg.php +++ b/lib/class-wp-theme-json-resolver-gutenberg.php @@ -764,8 +764,18 @@ private static function style_variation_has_scope( $variation, $scope ) { * @return array */ public static function get_style_variations( $scope = 'theme' ) { + return static::get_style_variations_from_directory( get_stylesheet_directory(), $scope ); + } + + /** + * Returns the style variation files defined by the theme (parent and child). + * + * @since 6.7.0 + * + * @return array An array of style variation files. + */ + protected static function get_style_variation_files_from_current_theme() { $variation_files = array(); - $variations = array(); $base_directory = get_stylesheet_directory() . '/styles'; $template_directory = get_template_directory() . '/styles'; if ( is_dir( $base_directory ) ) { @@ -783,6 +793,29 @@ public static function get_style_variations( $scope = 'theme' ) { } $variation_files = array_merge( $variation_files, $variation_files_parent ); } + + return $variation_files; + } + + /** + * Returns the style variations in the given directory. + * + * @since 6.7.0 + * + * @param string $directory The directory to get the style variations from. + * @param string $scope The scope or type of style variation to retrieve e.g. theme, block etc. + * @return array + */ + public static function get_style_variations_from_directory( $directory, $scope = 'theme' ) { + $variation_files = array(); + $variations = array(); + if ( is_dir( $directory ) ) { + if ( get_stylesheet_directory() === $directory ) { + $variation_files = static::get_style_variation_files_from_current_theme(); + } else { + $variation_files = static::recursively_iterate_json( $directory ); + } + } ksort( $variation_files ); foreach ( $variation_files as $path => $file ) { $decoded_file = self::read_json_file( $path ); diff --git a/lib/compat/plugin/fonts.php b/lib/compat/plugin/fonts.php new file mode 100644 index 0000000000000..f427f6110f610 --- /dev/null +++ b/lib/compat/plugin/fonts.php @@ -0,0 +1,43 @@ +post_type ) { + return; + } + + $font_files = get_post_meta( $post_id, '_wp_font_face_file', false ); + + if ( empty( $font_files ) ) { + return; + } + + $site_path = ''; + if ( is_multisite() && ! ( is_main_network() && is_main_site() ) ) { + $site_path = '/sites/' . get_current_blog_id(); + } + + $font_dir = path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path; + + foreach ( $font_files as $font_file ) { + $font_path = $font_dir . '/' . $font_file; + + if ( file_exists( $font_path ) ) { + wp_delete_file( $font_path ); + } + } +} +add_action( 'before_delete_post', 'gutenberg_before_delete_font_face', 10, 2 ); diff --git a/lib/compat/plugin/footnotes.php b/lib/compat/plugin/footnotes.php deleted file mode 100644 index a3d89aba0ae96..0000000000000 --- a/lib/compat/plugin/footnotes.php +++ /dev/null @@ -1,250 +0,0 @@ -post_parent; - - // Just making sure we're updating the right revision. - if ( $post->ID === $post_id ) { - $footnotes = get_post_meta( $post_id, 'footnotes', true ); - - if ( $footnotes ) { - // Can't use update_post_meta() because it doesn't allow revisions. - update_metadata( 'post', $wp_temporary_footnote_revision_id, 'footnotes', wp_slash( $footnotes ) ); - } - } - } - } - - if ( ! function_exists( 'wp_post_revision_meta_keys' ) ) { - add_action( 'rest_after_insert_post', 'wp_add_footnotes_revisions_to_post_meta' ); - add_action( 'rest_after_insert_page', 'wp_add_footnotes_revisions_to_post_meta' ); - } - } - - if ( ! function_exists( 'wp_restore_footnotes_from_revision' ) ) { - - /** - * Restores the footnotes meta value from the revision. - * - * @since 6.3.0 - * @since 6.4.0 Core added post meta revisions, so this is no longer needed. - * - * @param int $post_id The post ID. - * @param int $revision_id The revision ID. - */ - function wp_restore_footnotes_from_revision( $post_id, $revision_id ) { - $footnotes = get_post_meta( $revision_id, 'footnotes', true ); - - if ( $footnotes ) { - update_post_meta( $post_id, 'footnotes', wp_slash( $footnotes ) ); - } else { - delete_post_meta( $post_id, 'footnotes' ); - } - } - if ( ! function_exists( 'wp_post_revision_meta_keys' ) ) { - add_action( 'wp_restore_post_revision', 'wp_restore_footnotes_from_revision', 10, 2 ); - } - } - - if ( ! function_exists( '_wp_rest_api_autosave_meta' ) ) { - - /** - * The REST API autosave endpoint doesn't save meta, so we can use the - * `wp_creating_autosave` when it updates an exiting autosave, and - * `_wp_put_post_revision` when it creates a new autosave. - * - * @since 6.3.0 - * @since 6.4.0 Core added post meta revisions, so this is no longer needed. - * - * @param int|array $autosave The autosave ID or array. - */ - function _wp_rest_api_autosave_meta( $autosave ) { - // Ensure it's a REST API request. - if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) { - return; - } - - $body = rest_get_server()->get_raw_data(); - $body = json_decode( $body, true ); - - if ( ! isset( $body['meta']['footnotes'] ) ) { - return; - } - - // `wp_creating_autosave` passes the array, - // `_wp_put_post_revision` passes the ID. - $id = is_int( $autosave ) ? $autosave : $autosave['ID']; - - if ( ! $id ) { - return; - } - - // Can't use update_post_meta() because it doesn't allow revisions. - update_metadata( 'post', $id, 'footnotes', wp_slash( $body['meta']['footnotes'] ) ); - } - - if ( ! function_exists( 'wp_post_revision_meta_keys' ) ) { - // See https://github.com/WordPress/wordpress-develop/blob/2103cb9966e57d452c94218bbc3171579b536a40/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php#L391C1-L391C1. - add_action( 'wp_creating_autosave', '_wp_rest_api_autosave_meta' ); - // See https://github.com/WordPress/wordpress-develop/blob/2103cb9966e57d452c94218bbc3171579b536a40/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php#L398. - // Then https://github.com/WordPress/wordpress-develop/blob/2103cb9966e57d452c94218bbc3171579b536a40/src/wp-includes/revision.php#L367. - add_action( '_wp_put_post_revision', '_wp_rest_api_autosave_meta' ); - } - } - - if ( ! function_exists( '_wp_rest_api_force_autosave_difference' ) ) { - - /** - * This is a workaround for the autosave endpoint returning early if the - * revision field are equal. The problem is that "footnotes" is not real - * revision post field, so there's nothing to compare against. - * - * This trick sets the "footnotes" field (value doesn't matter), which will - * cause the autosave endpoint to always update the latest revision. That should - * be fine, it should be ok to update the revision even if nothing changed. Of - * course, this is temporary fix. - * - * @since 6.3.0 - * @since 6.4.0 Core added post meta revisions, so this is no longer needed. - * - * @param WP_Post $prepared_post The prepared post object. - * @param WP_REST_Request $request The request object. - * - * See https://github.com/WordPress/wordpress-develop/blob/2103cb9966e57d452c94218bbc3171579b536a40/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php#L365-L384. - * See https://github.com/WordPress/wordpress-develop/blob/2103cb9966e57d452c94218bbc3171579b536a40/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php#L219. - */ - function _wp_rest_api_force_autosave_difference( $prepared_post, $request ) { - // We only want to be altering POST requests. - if ( $request->get_method() !== 'POST' ) { - return $prepared_post; - } - - // Only alter requests for the '/autosaves' route. - if ( substr( $request->get_route(), -strlen( '/autosaves' ) ) !== '/autosaves' ) { - return $prepared_post; - } - - $prepared_post->footnotes = '[]'; - return $prepared_post; - } - if ( ! function_exists( 'wp_post_revision_meta_keys' ) ) { - add_filter( 'rest_pre_insert_post', '_wp_rest_api_force_autosave_difference', 10, 2 ); - } - } -} diff --git a/lib/compat/wordpress-6.4/block-hooks.php b/lib/compat/wordpress-6.4/block-hooks.php deleted file mode 100644 index f77582caf1345..0000000000000 --- a/lib/compat/wordpress-6.4/block-hooks.php +++ /dev/null @@ -1,377 +0,0 @@ - 'before', - 'after' => 'after', - 'firstChild' => 'first_child', - 'lastChild' => 'last_child', - ); - - $inserted_block_name = $metadata['name']; - foreach ( $block_hooks as $anchor_block_name => $position ) { - // Avoid infinite recursion (hooking to itself). - if ( $inserted_block_name === $anchor_block_name ) { - _doing_it_wrong( - __METHOD__, - __( 'Cannot hook block to itself.', 'gutenberg' ), - '6.4.0' - ); - continue; - } - - if ( ! isset( $property_mappings[ $position ] ) ) { - continue; - } - - $mapped_position = $property_mappings[ $position ]; - - gutenberg_add_hooked_block( $inserted_block_name, $mapped_position, $anchor_block_name ); - - $settings['block_hooks'][ $anchor_block_name ] = $mapped_position; - } - - // Copied from `get_block_editor_server_block_settings()`. - $fields_to_pick = array( - 'api_version' => 'apiVersion', - 'title' => 'title', - 'description' => 'description', - 'icon' => 'icon', - 'attributes' => 'attributes', - 'provides_context' => 'providesContext', - 'uses_context' => 'usesContext', - 'selectors' => 'selectors', - 'supports' => 'supports', - 'category' => 'category', - 'styles' => 'styles', - 'textdomain' => 'textdomain', - 'parent' => 'parent', - 'ancestor' => 'ancestor', - 'keywords' => 'keywords', - 'example' => 'example', - 'variations' => 'variations', - 'allowed_blocks' => 'allowedBlocks', - ); - // Add `block_hooks` to the list of fields to pick. - $fields_to_pick['block_hooks'] = 'blockHooks'; - - $exposed_settings = array_intersect_key( $settings, $fields_to_pick ); - - // TODO: Make work for blocks registered via direct call to gutenberg_add_hooked_block(). - wp_add_inline_script( - 'wp-blocks', - 'wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( array( $inserted_block_name => $exposed_settings ) ) . ');' - ); - - return $settings; -} - -/** - * Register a hooked block for automatic insertion into a given block hook. - * - * A block hook is specified by a block type and a relative position. The hooked block - * will be automatically inserted in the given position next to the "anchor" block - * whenever the latter is encountered. This applies both to the frontend and to the markup - * returned by the templates and patterns REST API endpoints. - * - * This is currently done by filtering parsed blocks as obtained from a block template, - * template part, or pattern, and injecting the hooked block where applicable. - * - * @todo In the long run, we'd likely want some sort of registry for hooked blocks. - * - * @param string $hooked_block The name of the block to insert. - * @param string $position The desired position of the hooked block, relative to its anchor block. - * Can be 'before', 'after', 'first_child', or 'last_child'. - * @param string $anchor_block The name of the block to insert the hooked block next to. - * @return void - */ -function gutenberg_add_hooked_block( $hooked_block, $position, $anchor_block ) { - $hooked_block_array = array( - 'blockName' => $hooked_block, - 'attrs' => array(), - 'innerHTML' => '', - 'innerContent' => array(), - 'innerBlocks' => array(), - ); - - $inserter = gutenberg_insert_hooked_block( $hooked_block_array, $position, $anchor_block ); - add_filter( 'gutenberg_serialize_block', $inserter, 10, 1 ); - - /* - * The block-types REST API controller uses objects of the `WP_Block_Type` class, which are - * in turn created upon block type registration. However, that class does not contain - * a `block_hooks` property (and is not easily extensible), so we have to use a different - * mechanism to communicate to the controller which hooked blocks have been registered for - * automatic insertion. We're doing so here (i.e. upon block registration), by adding a filter to - * the controller's response. - */ - $controller_extender = gutenberg_add_block_hooks_field_to_block_type_controller( $hooked_block, $position, $anchor_block ); - add_filter( 'rest_prepare_block_type', $controller_extender, 10, 2 ); -} - -/** - * Return a function that auto-inserts a block next to a given "anchor" block. - * - * This is a helper function used in the implementation of block hooks. - * It is not meant for public use. - * - * The auto-inserted block can be inserted before or after the anchor block, - * or as the first or last child of the anchor block. - * - * Note that the returned function mutates the automatically inserted block's - * designated parent block by inserting into the parent's `innerBlocks` array, - * and by updating the parent's `innerContent` array accordingly. - * - * @param array $inserted_block The block to insert. - * @param string $relative_position The position relative to the given block. - * Can be 'before', 'after', 'first_child', or 'last_child'. - * @param string $anchor_block_type The automatically inserted block will be inserted next to instances of this block type. - * @return callable A function that accepts a block's content and returns the content with the inserted block. - */ -function gutenberg_insert_hooked_block( $inserted_block, $relative_position, $anchor_block_type ) { - return function ( $block ) use ( $inserted_block, $relative_position, $anchor_block_type ) { - if ( $anchor_block_type === $block['blockName'] ) { - if ( 'first_child' === $relative_position ) { - array_unshift( $block['innerBlocks'], $inserted_block ); - // Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`) - // when rendering blocks, we also need to prepend a value (`null`, to mark a block - // location) to that array after HTML content for the inner blocks wrapper. - $chunk_index = 0; - for ( $index = $chunk_index; $index < count( $block['innerContent'] ); $index++ ) { - if ( is_null( $block['innerContent'][ $index ] ) ) { - $chunk_index = $index; - break; - } - } - array_splice( $block['innerContent'], $chunk_index, 0, array( null ) ); - } elseif ( 'last_child' === $relative_position ) { - array_push( $block['innerBlocks'], $inserted_block ); - // Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`) - // when rendering blocks, we also need to correctly append a value (`null`, to mark a block - // location) to that array before the remaining HTML content for the inner blocks wrapper. - $chunk_index = count( $block['innerContent'] ); - for ( $index = count( $block['innerContent'] ); $index > 0; $index-- ) { - if ( is_null( $block['innerContent'][ $index - 1 ] ) ) { - $chunk_index = $index; - break; - } - } - array_splice( $block['innerContent'], $chunk_index, 0, array( null ) ); - } - return $block; - } - - $anchor_block_index = array_search( $anchor_block_type, array_column( $block['innerBlocks'], 'blockName' ), true ); - if ( false !== $anchor_block_index && ( 'after' === $relative_position || 'before' === $relative_position ) ) { - if ( 'after' === $relative_position ) { - ++$anchor_block_index; - } - array_splice( $block['innerBlocks'], $anchor_block_index, 0, array( $inserted_block ) ); - - // Find matching `innerContent` chunk index. - $chunk_index = 0; - while ( $anchor_block_index > 0 ) { - if ( ! is_string( $block['innerContent'][ $chunk_index ] ) ) { - --$anchor_block_index; - } - ++$chunk_index; - } - // Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`) - // when rendering blocks, we also need to insert a value (`null`, to mark a block - // location) into that array. - array_splice( $block['innerContent'], $chunk_index, 0, array( null ) ); - } - return $block; - }; -} - -/** - * Add block hooks information to a block type's controller. - * - * @param array $inserted_block_type The type of block to insert. - * @param string $position The position relative to the anchor block. - * Can be 'before', 'after', 'first_child', or 'last_child'. - * @param string $anchor_block_type The hooked block will be inserted next to instances of this block type. - * @return callable A filter for the `rest_prepare_block_type` hook that adds a `block_hooks` field to the network response. - */ -function gutenberg_add_block_hooks_field_to_block_type_controller( $inserted_block_type, $position, $anchor_block_type ) { - return function ( $response, $block_type ) use ( $inserted_block_type, $position, $anchor_block_type ) { - if ( $block_type->name !== $inserted_block_type ) { - return $response; - } - - $data = $response->get_data(); - if ( ! isset( $data['block_hooks'] ) ) { - $data['block_hooks'] = array(); - } - $data['block_hooks'][ $anchor_block_type ] = $position; - $response->set_data( $data ); - return $response; - }; -} - -/** - * Parse and reserialize block templates to allow running filters. - * - * By parsing a block template's content and then reserializing it - * via `gutenberg_serialize_blocks()`, we are able to run filters - * on the parsed blocks. This allows us to modify (parsed) blocks during - * depth-first traversal already provided by the serialization process, - * rather than having to do so in a separate pass. - * - * @param WP_Block_Template[] $query_result Array of found block templates. - * @return WP_Block_Template[] Updated array of found block templates. - */ -function gutenberg_parse_and_serialize_block_templates( $query_result ) { - foreach ( $query_result as $block_template ) { - if ( empty( $block_template->content ) || 'custom' === $block_template->source ) { - continue; - } - $blocks = parse_blocks( $block_template->content ); - $block_template->content = gutenberg_serialize_blocks( $blocks ); - } - - return $query_result; -} - -/** - * Filters the block template object after it has been (potentially) fetched from the theme file. - * - * By parsing a block template's content and then reserializing it - * via `gutenberg_serialize_blocks()`, we are able to run filters - * on the parsed blocks. This allows us to modify (parsed) blocks during - * depth-first traversal already provided by the serialization process, - * rather than having to do so in a separate pass. - * - * @param WP_Block_Template|null $block_template The found block template, or null if there is none. - */ -function gutenberg_parse_and_serialize_blocks( $block_template ) { - if ( empty( $block_template->content ) ) { - return $block_template; - } - - $blocks = parse_blocks( $block_template->content ); - $block_template->content = gutenberg_serialize_blocks( $blocks ); - - return $block_template; -} - -/** - * Register the `block_hooks` field for the block-types REST API controller. - * - * @return void - */ -function gutenberg_register_block_hooks_rest_field() { - register_rest_field( - 'block-type', - 'block_hooks', - array( - 'schema' => array( - 'description' => __( 'This block is automatically inserted near any occurrence of the block types used as keys of this map, into a relative position given by the corresponding value.', 'gutenberg' ), - 'type' => 'object', - 'patternProperties' => array( - '^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$' => array( - 'type' => 'string', - 'enum' => array( 'before', 'after', 'first_child', 'last_child' ), - ), - ), - ), - ) - ); -} - -// Install the polyfill for Block Hooks only if it isn't already handled in WordPress core. -if ( ! function_exists( 'traverse_and_serialize_blocks' ) ) { - add_filter( 'block_type_metadata_settings', 'gutenberg_add_hooked_blocks', 10, 2 ); - add_filter( 'get_block_templates', 'gutenberg_parse_and_serialize_block_templates', 10, 1 ); - add_filter( 'get_block_file_template', 'gutenberg_parse_and_serialize_blocks', 10, 1 ); - add_action( 'rest_api_init', 'gutenberg_register_block_hooks_rest_field' ); -} - -// Helper functions. -// ----------------- -// The sole purpose of the following two functions (`gutenberg_serialize_block` -// and `gutenberg_serialize_blocks`), which are otherwise copies of their unprefixed -// counterparts (`serialize_block` and `serialize_blocks`) is to apply a filter -// (also called `gutenberg_serialize_block`) as an entry point for modifications -// to the parsed blocks. - -/** - * Filterable version of `serialize_block()`. - * - * This function is identical to `serialize_block()`, except that it applies - * the `gutenberg_serialize_block` filter to each block before it is serialized. - * - * @param array $block The block to be serialized. - * @return string The serialized block. - * - * @see serialize_block() - */ -function gutenberg_serialize_block( $block ) { - $block_content = ''; - - /** - * Filters a parsed block before it is serialized. - * - * @param array $block The block to be serialized. - */ - $block = apply_filters( 'gutenberg_serialize_block', $block ); - - $index = 0; - foreach ( $block['innerContent'] as $chunk ) { - if ( is_string( $chunk ) ) { - $block_content .= $chunk; - } else { // Compare to WP_Block::render(). - $inner_block = $block['innerBlocks'][ $index++ ]; - $block_content .= gutenberg_serialize_block( $inner_block ); - } - } - - if ( ! is_array( $block['attrs'] ) ) { - $block['attrs'] = array(); - } - - return get_comment_delimited_block_content( - $block['blockName'], - $block['attrs'], - $block_content - ); -} - -/** - * Filterable version of `serialize_blocks()`. - * - * This function is identical to `serialize_blocks()`, except that it applies - * the `gutenberg_serialize_block` filter to each block before it is serialized. - * - * @param array $blocks The blocks to be serialized. - * @return string[] The serialized blocks. - * - * @see serialize_blocks() - */ -function gutenberg_serialize_blocks( $blocks ) { - return implode( '', array_map( 'gutenberg_serialize_block', $blocks ) ); -} diff --git a/lib/compat/wordpress-6.4/blocks.php b/lib/compat/wordpress-6.4/blocks.php deleted file mode 100644 index 74fa9253e45d5..0000000000000 --- a/lib/compat/wordpress-6.4/blocks.php +++ /dev/null @@ -1,23 +0,0 @@ -get_data(); - - if ( empty( $data['content'] ) ) { - return $response; - } - - $blocks = parse_blocks( $data['content'] ); - $data['content'] = gutenberg_serialize_blocks( $blocks ); // Serialize or render? - - return rest_ensure_response( $data ); - } -} diff --git a/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php b/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php deleted file mode 100644 index 4c7df97c33e57..0000000000000 --- a/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php +++ /dev/null @@ -1,164 +0,0 @@ -get_parent( $request['parent'] ); - $global_styles_config = $this->get_decoded_global_styles_json( $post->post_content ); - - if ( is_wp_error( $global_styles_config ) ) { - return $global_styles_config; - } - - $fields = $this->get_fields_for_response( $request ); - $data = array(); - - if ( ! empty( $global_styles_config['styles'] ) || ! empty( $global_styles_config['settings'] ) ) { - $global_styles_config = ( new WP_Theme_JSON_Gutenberg( $global_styles_config, 'custom' ) )->get_raw_data(); - if ( rest_is_field_included( 'settings', $fields ) ) { - $data['settings'] = ! empty( $global_styles_config['settings'] ) ? $global_styles_config['settings'] : new stdClass(); - } - if ( rest_is_field_included( 'styles', $fields ) ) { - $data['styles'] = ! empty( $global_styles_config['styles'] ) ? $global_styles_config['styles'] : new stdClass(); - } - } - - if ( rest_is_field_included( 'author', $fields ) ) { - $data['author'] = (int) $post->post_author; - } - - if ( rest_is_field_included( 'date', $fields ) ) { - $data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date ); - } - - if ( rest_is_field_included( 'date_gmt', $fields ) ) { - $data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt ); - } - - if ( rest_is_field_included( 'id', $fields ) ) { - $data['id'] = (int) $post->ID; - } - - if ( rest_is_field_included( 'modified', $fields ) ) { - $data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified ); - } - - if ( rest_is_field_included( 'modified_gmt', $fields ) ) { - $data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt ); - } - - if ( rest_is_field_included( 'parent', $fields ) ) { - $data['parent'] = (int) $parent->ID; - } - - $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 revision's schema, conforming to JSON Schema. - * - * @since 6.3.0 - * - * @return array Item schema data. - */ - public function get_item_schema() { - if ( $this->schema ) { - return $this->add_additional_fields_schema( $this->schema ); - } - - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => "{$this->parent_post_type}-revision", - 'type' => 'object', - // Base properties for every Revision. - 'properties' => array( - - /* - * Adds settings and styles from the WP_REST_Revisions_Controller item fields. - * Leaves out GUID as global styles shouldn't be accessible via URL. - */ - 'author' => array( - 'description' => __( 'The ID for the author of the revision.', 'gutenberg' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'date' => array( - 'description' => __( "The date the revision was published, in the site's timezone.", 'gutenberg' ), - 'type' => 'string', - 'format' => 'date-time', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'date_gmt' => array( - 'description' => __( 'The date the revision was published, as GMT.', 'gutenberg' ), - 'type' => 'string', - 'format' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'id' => array( - 'description' => __( 'Unique identifier for the revision.', 'gutenberg' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'modified' => array( - 'description' => __( "The date the revision was last modified, in the site's timezone.", 'gutenberg' ), - 'type' => 'string', - 'format' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'modified_gmt' => array( - 'description' => __( 'The date the revision was last modified, as GMT.', 'gutenberg' ), - 'type' => 'string', - 'format' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'parent' => array( - 'description' => __( 'The ID for the parent of the revision.', 'gutenberg' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit', 'embed' ), - ), - - // Adds settings and styles from the WP_REST_Global_Styles_Controller parent schema. - 'styles' => array( - 'description' => __( 'Global styles.', 'gutenberg' ), - 'type' => array( 'object' ), - 'context' => array( 'view', 'edit' ), - ), - 'settings' => array( - 'description' => __( 'Global settings.', 'gutenberg' ), - 'type' => array( 'object' ), - 'context' => array( 'view', 'edit' ), - ), - ), - ); - - $this->schema = $schema; - - return $this->add_additional_fields_schema( $this->schema ); - } -} diff --git a/lib/compat/wordpress-6.4/class-gutenberg-rest-templates-controller-6-4.php b/lib/compat/wordpress-6.4/class-gutenberg-rest-templates-controller-6-4.php deleted file mode 100644 index ec969519f9ac4..0000000000000 --- a/lib/compat/wordpress-6.4/class-gutenberg-rest-templates-controller-6-4.php +++ /dev/null @@ -1,75 +0,0 @@ -get_fields_for_response( $request ); - - $response = parent::prepare_item_for_response( $item, $request ); - - if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { - $links = $this->prepare_revision_links( $template ); - $response->add_links( $links ); - if ( ! empty( $links['self']['href'] ) ) { - $actions = $this->get_available_actions(); - $self = $links['self']['href']; - foreach ( $actions as $rel ) { - $response->add_link( $rel, $self ); - } - } - } - - return $response; - } - - /** - * Adds revisions to links. - * - * @param WP_Block_Template $template Template instance. - * @return array Links for the given post. - */ - protected function prepare_revision_links( $template ) { - $links = array(); - - if ( post_type_supports( $this->post_type, 'revisions' ) && (int) $template->wp_id ) { - $revisions = wp_get_latest_revision_id_and_total_count( (int) $template->wp_id ); - $revisions_count = ! is_wp_error( $revisions ) ? $revisions['count'] : 0; - $revisions_base = sprintf( '/%s/%s/%s/revisions', $this->namespace, $this->rest_base, $template->id ); - - $links['version-history'] = array( - 'href' => rest_url( $revisions_base ), - 'count' => $revisions_count, - ); - - if ( $revisions_count > 0 ) { - $links['predecessor-version'] = array( - 'href' => rest_url( $revisions_base . '/' . $revisions['latest_id'] ), - 'id' => $revisions['latest_id'], - ); - } - } - - return $links; - } -} diff --git a/lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face-resolver.php b/lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face-resolver.php deleted file mode 100644 index 556663b881366..0000000000000 --- a/lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face-resolver.php +++ /dev/null @@ -1,183 +0,0 @@ - $src_url ) { - // Skip if the src doesn't start with the placeholder, as there's nothing to replace. - if ( ! str_starts_with( $src_url, $placeholder ) ) { - continue; - } - - $src_file = str_replace( $placeholder, '', $src_url ); - $src[ $src_key ] = get_theme_file_uri( $src_file ); - } - - return $src; - } - - /** - * Converts all first dimension keys into kebab-case. - * - * @since 6.4.0 - * - * @param array $data The array to process. - * @return array Data with first dimension keys converted into kebab-case. - */ - private static function to_kebab_case( array $data ) { - foreach ( $data as $key => $value ) { - $kebab_case = _wp_to_kebab_case( $key ); - $data[ $kebab_case ] = $value; - if ( $kebab_case !== $key ) { - unset( $data[ $key ] ); - } - } - - return $data; - } - } -} diff --git a/lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face.php b/lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face.php deleted file mode 100644 index 6bea6eb86cc71..0000000000000 --- a/lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face.php +++ /dev/null @@ -1,435 +0,0 @@ - '', - 'font-style' => 'normal', - 'font-weight' => '400', - 'font-display' => 'fallback', - ); - - /** - * Valid font-face property names. - * - * @since 6.4.0 - * - * @var string[] - */ - private $valid_font_face_properties = 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', - ); - - /** - * Valid font-display values. - * - * @since 6.4.0 - * - * @var string[] - */ - private $valid_font_display = array( 'auto', 'block', 'fallback', 'swap', 'optional' ); - - /** - * Array of font-face style tag's attribute(s) - * where the key is the attribute name and the - * value is its value. - * - * @since 6.4.0 - * - * @var string[] - */ - private $style_tag_attrs = array(); - - /** - * Creates and initializes an instance of WP_Font_Face. - * - * @since 6.4.0 - */ - public function __construct() { - if ( - function_exists( 'is_admin' ) && ! is_admin() - && - function_exists( 'current_theme_supports' ) && ! current_theme_supports( 'html5', 'style' ) - ) { - $this->style_tag_attrs = array( 'type' => 'text/css' ); - } - } - - /** - * Generates and prints the `@font-face` styles for the given fonts. - * - * @since 6.4.0 - * - * @param array[][] $fonts Optional. The font-families and their font variations. - * See {@see wp_print_font_faces()} for the supported fields. - * Default empty array. - */ - public function generate_and_print( array $fonts ) { - $fonts = $this->validate_fonts( $fonts ); - - // Bail out if there are no fonts are given to process. - if ( empty( $fonts ) ) { - return; - } - - $css = $this->get_css( $fonts ); - - /* - * The font-face CSS is contained within and open a ` inside an HTML comment. - * - STYLE content is raw text. - * - TITLE content is plain text but character references are decoded. - * - TEXTAREA content is plain text but character references are decoded. - * - XMP (deprecated) content is raw text. - * - * ### Modifying HTML attributes for a found tag - * - * Once you've found the start of an opening tag you can modify - * any number of the attributes on that tag. You can set a new - * value for an attribute, remove the entire attribute, or do - * nothing and move on to the next opening tag. - * - * Example: - * - * if ( $tags->next_tag( array( 'class_name' => 'wp-group-block' ) ) ) { - * $tags->set_attribute( 'title', 'This groups the contained content.' ); - * $tags->remove_attribute( 'data-test-id' ); - * } - * - * If `set_attribute()` is called for an existing attribute it will - * overwrite the existing value. Similarly, calling `remove_attribute()` - * for a non-existing attribute has no effect on the document. Both - * of these methods are safe to call without knowing if a given attribute - * exists beforehand. - * - * ### Modifying CSS classes for a found tag - * - * The tag processor treats the `class` attribute as a special case. - * Because it's a common operation to add or remove CSS classes, this - * interface adds helper methods to make that easier. - * - * As with attribute values, adding or removing CSS classes is a safe - * operation that doesn't require checking if the attribute or class - * exists before making changes. If removing the only class then the - * entire `class` attribute will be removed. - * - * Example: - * - * // from `Yippee!` - * // to `Yippee!` - * $tags->add_class( 'is-active' ); - * - * // from `Yippee!` - * // to `Yippee!` - * $tags->add_class( 'is-active' ); - * - * // from `Yippee!` - * // to `Yippee!` - * $tags->add_class( 'is-active' ); - * - * // from `` - * // to ` - * $tags->remove_class( 'rugby' ); - * - * // from `` - * // to ` - * $tags->remove_class( 'rugby' ); - * - * // from `` - * // to ` - * $tags->remove_class( 'rugby' ); - * - * When class changes are enqueued but a direct change to `class` is made via - * `set_attribute` then the changes to `set_attribute` (or `remove_attribute`) - * will take precedence over those made through `add_class` and `remove_class`. - * - * ### Bookmarks - * - * While scanning through the input HTMl document it's possible to set - * a named bookmark when a particular tag is found. Later on, after - * continuing to scan other tags, it's possible to `seek` to one of - * the set bookmarks and then proceed again from that point forward. - * - * Because bookmarks create processing overhead one should avoid - * creating too many of them. As a rule, create only bookmarks - * of known string literal names; avoid creating "mark_{$index}" - * and so on. It's fine from a performance standpoint to create a - * bookmark and update it frequently, such as within a loop. - * - * $total_todos = 0; - * while ( $p->next_tag( array( 'tag_name' => 'UL', 'class_name' => 'todo' ) ) ) { - * $p->set_bookmark( 'list-start' ); - * while ( $p->next_tag( array( 'tag_closers' => 'visit' ) ) ) { - * if ( 'UL' === $p->get_tag() && $p->is_tag_closer() ) { - * $p->set_bookmark( 'list-end' ); - * $p->seek( 'list-start' ); - * $p->set_attribute( 'data-contained-todos', (string) $total_todos ); - * $total_todos = 0; - * $p->seek( 'list-end' ); - * break; - * } - * - * if ( 'LI' === $p->get_tag() && ! $p->is_tag_closer() ) { - * $total_todos++; - * } - * } - * } - * - * ## Tokens and finer-grained processing. - * - * It's possible to scan through every lexical token in the - * HTML document using the `next_token()` function. This - * alternative form takes no argument and provides no built-in - * query syntax. - * - * Example: - * - * $title = '(untitled)'; - * $text = ''; - * while ( $processor->next_token() ) { - * switch ( $processor->get_token_name() ) { - * case '#text': - * $text .= $processor->get_modifiable_text(); - * break; - * - * case 'BR': - * $text .= "\n"; - * break; - * - * case 'TITLE': - * $title = $processor->get_modifiable_text(); - * break; - * } - * } - * return trim( "# {$title}\n\n{$text}" ); - * - * ### Tokens and _modifiable text_. - * - * #### Special "atomic" HTML elements. - * - * Not all HTML elements are able to contain other elements inside of them. - * For instance, the contents inside a TITLE element are plaintext (except - * that character references like & will be decoded). This means that - * if the string `` appears inside a TITLE element, then it's not an - * image tag, but rather it's text describing an image tag. Likewise, the - * contents of a SCRIPT or STYLE element are handled entirely separately in - * a browser than the contents of other elements because they represent a - * different language than HTML. - * - * For these elements the Tag Processor treats the entire sequence as one, - * from the opening tag, including its contents, through its closing tag. - * This means that the it's not possible to match the closing tag for a - * SCRIPT element unless it's unexpected; the Tag Processor already matched - * it when it found the opening tag. - * - * The inner contents of these elements are that element's _modifiable text_. - * - * The special elements are: - * - `SCRIPT` whose contents are treated as raw plaintext but supports a legacy - * style of including Javascript inside of HTML comments to avoid accidentally - * closing the SCRIPT from inside a Javascript string. E.g. `console.log( '' )`. - * - `TITLE` and `TEXTAREA` whose contents are treated as plaintext and then any - * character references are decoded. E.g. `1 < 2 < 3` becomes `1 < 2 < 3`. - * - `IFRAME`, `NOSCRIPT`, `NOEMBED`, `NOFRAME`, `STYLE` whose contents are treated as - * raw plaintext and left as-is. E.g. `1 < 2 < 3` remains `1 < 2 < 3`. - * - * #### Other tokens with modifiable text. - * - * There are also non-elements which are void/self-closing in nature and contain - * modifiable text that is part of that individual syntax token itself. - * - * - `#text` nodes, whose entire token _is_ the modifiable text. - * - HTML comments and tokens that become comments due to some syntax error. The - * text for these tokens is the portion of the comment inside of the syntax. - * E.g. for `` the text is `" comment "` (note the spaces are included). - * - `CDATA` sections, whose text is the content inside of the section itself. E.g. for - * `` the text is `"some content"` (with restrictions [1]). - * - "Funky comments," which are a special case of invalid closing tags whose name is - * invalid. The text for these nodes is the text that a browser would transform into - * an HTML comment when parsing. E.g. for `` the text is `%post_author`. - * - `DOCTYPE` declarations like `` which have no closing tag. - * - XML Processing instruction nodes like `` (with restrictions [2]). - * - The empty end tag `` which is ignored in the browser and DOM. - * - * [1]: There are no CDATA sections in HTML. When encountering `` becomes a bogus HTML comment, meaning there can be no CDATA - * section in an HTML document containing `>`. The Tag Processor will first find - * all valid and bogus HTML comments, and then if the comment _would_ have been a - * CDATA section _were they to exist_, it will indicate this as the type of comment. - * - * [2]: XML allows a broader range of characters in a processing instruction's target name - * and disallows "xml" as a name, since it's special. The Tag Processor only recognizes - * target names with an ASCII-representable subset of characters. It also exhibits the - * same constraint as with CDATA sections, in that `>` cannot exist within the token - * since Processing Instructions do no exist within HTML and their syntax transforms - * into a bogus comment in the DOM. - * - * ## Design and limitations - * - * The Tag Processor is designed to linearly scan HTML documents and tokenize - * HTML tags and their attributes. It's designed to do this as efficiently as - * possible without compromising parsing integrity. Therefore it will be - * slower than some methods of modifying HTML, such as those incorporating - * over-simplified PCRE patterns, but will not introduce the defects and - * failures that those methods bring in, which lead to broken page renders - * and often to security vulnerabilities. On the other hand, it will be faster - * than full-blown HTML parsers such as DOMDocument and use considerably - * less memory. It requires a negligible memory overhead, enough to consider - * it a zero-overhead system. - * - * The performance characteristics are maintained by avoiding tree construction - * and semantic cleanups which are specified in HTML5. Because of this, for - * example, it's not possible for the Tag Processor to associate any given - * opening tag with its corresponding closing tag, or to return the inner markup - * inside an element. Systems may be built on top of the Tag Processor to do - * this, but the Tag Processor is and should be constrained so it can remain an - * efficient, low-level, and reliable HTML scanner. - * - * The Tag Processor's design incorporates a "garbage-in-garbage-out" philosophy. - * HTML5 specifies that certain invalid content be transformed into different forms - * for display, such as removing null bytes from an input document and replacing - * invalid characters with the Unicode replacement character `U+FFFD` (visually "�"). - * Where errors or transformations exist within the HTML5 specification, the Tag Processor - * leaves those invalid inputs untouched, passing them through to the final browser - * to handle. While this implies that certain operations will be non-spec-compliant, - * such as reading the value of an attribute with invalid content, it also preserves a - * simplicity and efficiency for handling those error cases. - * - * Most operations within the Tag Processor are designed to minimize the difference - * between an input and output document for any given change. For example, the - * `add_class` and `remove_class` methods preserve whitespace and the class ordering - * within the `class` attribute; and when encountering tags with duplicated attributes, - * the Tag Processor will leave those invalid duplicate attributes where they are but - * update the proper attribute which the browser will read for parsing its value. An - * exception to this rule is that all attribute updates store their values as - * double-quoted strings, meaning that attributes on input with single-quoted or - * unquoted values will appear in the output with double-quotes. - * - * ### Scripting Flag - * - * The Tag Processor parses HTML with the "scripting flag" disabled. This means - * that it doesn't run any scripts while parsing the page. In a browser with - * JavaScript enabled, for example, the script can change the parse of the - * document as it loads. On the server, however, evaluating JavaScript is not - * only impractical, but also unwanted. - * - * Practically this means that the Tag Processor will descend into NOSCRIPT - * elements and process its child tags. Were the scripting flag enabled, such - * as in a typical browser, the contents of NOSCRIPT are skipped entirely. - * - * This allows the HTML API to process the content that will be presented in - * a browser when scripting is disabled, but it offers a different view of a - * page than most browser sessions will experience. E.g. the tags inside the - * NOSCRIPT disappear. - * - * ### Text Encoding - * - * The Tag Processor assumes that the input HTML document is encoded with a - * text encoding compatible with 7-bit ASCII's '<', '>', '&', ';', '/', '=', - * "'", '"', 'a' - 'z', 'A' - 'Z', and the whitespace characters ' ', tab, - * carriage-return, newline, and form-feed. - * - * In practice, this includes almost every single-byte encoding as well as - * UTF-8. Notably, however, it does not include UTF-16. If providing input - * that's incompatible, then convert the encoding beforehand. - * - * @since 6.2.0 - * @since 6.2.1 Fix: Support for various invalid comments; attribute updates are case-insensitive. - * @since 6.3.2 Fix: Skip HTML-like content inside rawtext elements such as STYLE. - * @since 6.5.0 Pauses processor when input ends in an incomplete syntax token. - * Introduces "special" elements which act like void elements, e.g. TITLE, STYLE. - * Allows scanning through all tokens and processing modifiable text, where applicable. - */ -class Gutenberg_HTML_Tag_Processor_6_5 { - /** - * The maximum number of bookmarks allowed to exist at - * any given time. - * - * @since 6.2.0 - * @var int - * - * @see WP_HTML_Tag_Processor::set_bookmark() - */ - const MAX_BOOKMARKS = 10; - - /** - * Maximum number of times seek() can be called. - * Prevents accidental infinite loops. - * - * @since 6.2.0 - * @var int - * - * @see WP_HTML_Tag_Processor::seek() - */ - const MAX_SEEK_OPS = 1000; - - /** - * The HTML document to parse. - * - * @since 6.2.0 - * @var string - */ - protected $html; - - /** - * The last query passed to next_tag(). - * - * @since 6.2.0 - * @var array|null - */ - private $last_query; - - /** - * The tag name this processor currently scans for. - * - * @since 6.2.0 - * @var string|null - */ - private $sought_tag_name; - - /** - * The CSS class name this processor currently scans for. - * - * @since 6.2.0 - * @var string|null - */ - private $sought_class_name; - - /** - * The match offset this processor currently scans for. - * - * @since 6.2.0 - * @var int|null - */ - private $sought_match_offset; - - /** - * Whether to visit tag closers, e.g. , when walking an input document. - * - * @since 6.2.0 - * @var bool - */ - private $stop_on_tag_closers; - - /** - * Specifies mode of operation of the parser at any given time. - * - * | State | Meaning | - * | ----------------|----------------------------------------------------------------------| - * | *Ready* | The parser is ready to run. | - * | *Complete* | There is nothing left to parse. | - * | *Incomplete* | The HTML ended in the middle of a token; nothing more can be parsed. | - * | *Matched tag* | Found an HTML tag; it's possible to modify its attributes. | - * | *Text node* | Found a #text node; this is plaintext and modifiable. | - * | *CDATA node* | Found a CDATA section; this is modifiable. | - * | *Comment* | Found a comment or bogus comment; this is modifiable. | - * | *Presumptuous* | Found an empty tag closer: ``. | - * | *Funky comment* | Found a tag closer with an invalid tag name; this is modifiable. | - * - * @since 6.5.0 - * - * @see WP_HTML_Tag_Processor::STATE_READY - * @see WP_HTML_Tag_Processor::STATE_COMPLETE - * @see WP_HTML_Tag_Processor::STATE_INCOMPLETE_INPUT - * @see WP_HTML_Tag_Processor::STATE_MATCHED_TAG - * @see WP_HTML_Tag_Processor::STATE_TEXT_NODE - * @see WP_HTML_Tag_Processor::STATE_CDATA_NODE - * @see WP_HTML_Tag_Processor::STATE_COMMENT - * @see WP_HTML_Tag_Processor::STATE_DOCTYPE - * @see WP_HTML_Tag_Processor::STATE_PRESUMPTUOUS_TAG - * @see WP_HTML_Tag_Processor::STATE_FUNKY_COMMENT - * - * @var string - */ - protected $parser_state = self::STATE_READY; - - /** - * What kind of syntax token became an HTML comment. - * - * Since there are many ways in which HTML syntax can create an HTML comment, - * this indicates which of those caused it. This allows the Tag Processor to - * represent more from the original input document than would appear in the DOM. - * - * @since 6.5.0 - * - * @var string|null - */ - protected $comment_type = null; - - /** - * How many bytes from the original HTML document have been read and parsed. - * - * This value points to the latest byte offset in the input document which - * has been already parsed. It is the internal cursor for the Tag Processor - * and updates while scanning through the HTML tokens. - * - * @since 6.2.0 - * @var int - */ - private $bytes_already_parsed = 0; - - /** - * Byte offset in input document where current token starts. - * - * Example: - * - *
... - * 01234 - * - token starts at 0 - * - * @since 6.5.0 - * - * @var int|null - */ - private $token_starts_at; - - /** - * Byte length of current token. - * - * Example: - * - *
... - * 012345678901234 - * - token length is 14 - 0 = 14 - * - * a is a token. - * 0123456789 123456789 123456789 - * - token length is 17 - 2 = 15 - * - * @since 6.5.0 - * - * @var int|null - */ - private $token_length; - - /** - * Byte offset in input document where current tag name starts. - * - * Example: - * - *
... - * 01234 - * - tag name starts at 1 - * - * @since 6.2.0 - * - * @var int|null - */ - private $tag_name_starts_at; - - /** - * Byte length of current tag name. - * - * Example: - * - *
... - * 01234 - * --- tag name length is 3 - * - * @since 6.2.0 - * - * @var int|null - */ - private $tag_name_length; - - /** - * Byte offset into input document where current modifiable text starts. - * - * @since 6.5.0 - * - * @var int - */ - private $text_starts_at; - - /** - * Byte length of modifiable text. - * - * @since 6.5.0 - * - * @var string - */ - private $text_length; - - /** - * Whether the current tag is an opening tag, e.g.
, or a closing tag, e.g.
. - * - * @var bool - */ - private $is_closing_tag; - - /** - * Lazily-built index of attributes found within an HTML tag, keyed by the attribute name. - * - * Example: - * - * // Supposing the parser is working through this content - * // and stops after recognizing the `id` attribute. - * //
- * // ^ parsing will continue from this point. - * $this->attributes = array( - * 'id' => new WP_HTML_Attribute_Token( 'id', 9, 6, 5, 11, false ) - * ); - * - * // When picking up parsing again, or when asking to find the - * // `class` attribute we will continue and add to this array. - * $this->attributes = array( - * 'id' => new WP_HTML_Attribute_Token( 'id', 9, 6, 5, 11, false ), - * 'class' => new WP_HTML_Attribute_Token( 'class', 23, 7, 17, 13, false ) - * ); - * - * // Note that only the `class` attribute value is stored in the index. - * // That's because it is the only value used by this class at the moment. - * - * @since 6.2.0 - * @var WP_HTML_Attribute_Token[] - */ - private $attributes = array(); - - /** - * Tracks spans of duplicate attributes on a given tag, used for removing - * all copies of an attribute when calling `remove_attribute()`. - * - * @since 6.3.2 - * - * @var (WP_HTML_Span[])[]|null - */ - private $duplicate_attributes = null; - - /** - * Which class names to add or remove from a tag. - * - * These are tracked separately from attribute updates because they are - * semantically distinct, whereas this interface exists for the common - * case of adding and removing class names while other attributes are - * generally modified as with DOM `setAttribute` calls. - * - * When modifying an HTML document these will eventually be collapsed - * into a single `set_attribute( 'class', $changes )` call. - * - * Example: - * - * // Add the `wp-block-group` class, remove the `wp-group` class. - * $classname_updates = array( - * // Indexed by a comparable class name. - * 'wp-block-group' => WP_HTML_Tag_Processor::ADD_CLASS, - * 'wp-group' => WP_HTML_Tag_Processor::REMOVE_CLASS - * ); - * - * @since 6.2.0 - * @var bool[] - */ - private $classname_updates = array(); - - /** - * Tracks a semantic location in the original HTML which - * shifts with updates as they are applied to the document. - * - * @since 6.2.0 - * @var WP_HTML_Span[] - */ - protected $bookmarks = array(); - - const ADD_CLASS = true; - const REMOVE_CLASS = false; - const SKIP_CLASS = null; - - /** - * Lexical replacements to apply to input HTML document. - * - * "Lexical" in this class refers to the part of this class which - * operates on pure text _as text_ and not as HTML. There's a line - * between the public interface, with HTML-semantic methods like - * `set_attribute` and `add_class`, and an internal state that tracks - * text offsets in the input document. - * - * When higher-level HTML methods are called, those have to transform their - * operations (such as setting an attribute's value) into text diffing - * operations (such as replacing the sub-string from indices A to B with - * some given new string). These text-diffing operations are the lexical - * updates. - * - * As new higher-level methods are added they need to collapse their - * operations into these lower-level lexical updates since that's the - * Tag Processor's internal language of change. Any code which creates - * these lexical updates must ensure that they do not cross HTML syntax - * boundaries, however, so these should never be exposed outside of this - * class or any classes which intentionally expand its functionality. - * - * These are enqueued while editing the document instead of being immediately - * applied to avoid processing overhead, string allocations, and string - * copies when applying many updates to a single document. - * - * Example: - * - * // Replace an attribute stored with a new value, indices - * // sourced from the lazily-parsed HTML recognizer. - * $start = $attributes['src']->start; - * $length = $attributes['src']->length; - * $modifications[] = new WP_HTML_Text_Replacement( $start, $length, $new_value ); - * - * // Correspondingly, something like this will appear in this array. - * $lexical_updates = array( - * WP_HTML_Text_Replacement( 14, 28, 'https://my-site.my-domain/wp-content/uploads/2014/08/kittens.jpg' ) - * ); - * - * @since 6.2.0 - * @var WP_HTML_Text_Replacement[] - */ - protected $lexical_updates = array(); - - /** - * Tracks and limits `seek()` calls to prevent accidental infinite loops. - * - * @since 6.2.0 - * @var int - * - * @see WP_HTML_Tag_Processor::seek() - */ - protected $seek_count = 0; - - /** - * Constructor. - * - * @since 6.2.0 - * - * @param string $html HTML to process. - */ - public function __construct( $html ) { - $this->html = $html; - } - - /** - * Finds the next tag matching the $query. - * - * @since 6.2.0 - * @since 6.5.0 No longer processes incomplete tokens at end of document; pauses the processor at start of token. - * - * @param array|string|null $query { - * Optional. Which tag name to find, having which class, etc. Default is to find any tag. - * - * @type string|null $tag_name Which tag to find, or `null` for "any tag." - * @type int|null $match_offset Find the Nth tag matching all search criteria. - * 1 for "first" tag, 3 for "third," etc. - * Defaults to first tag. - * @type string|null $class_name Tag must contain this whole class name to match. - * @type string|null $tag_closers "visit" or "skip": whether to stop on tag closers, e.g.
. - * } - * @return bool Whether a tag was matched. - */ - public function next_tag( $query = null ) { - $this->parse_query( $query ); - $already_found = 0; - - do { - if ( false === $this->next_token() ) { - return false; - } - - if ( self::STATE_MATCHED_TAG !== $this->parser_state ) { - continue; - } - - if ( $this->matches() ) { - ++$already_found; - } - } while ( $already_found < $this->sought_match_offset ); - - return true; - } - - /** - * Finds the next token in the HTML document. - * - * An HTML document can be viewed as a stream of tokens, - * where tokens are things like HTML tags, HTML comments, - * text nodes, etc. This method finds the next token in - * the HTML document and returns whether it found one. - * - * If it starts parsing a token and reaches the end of the - * document then it will seek to the start of the last - * token and pause, returning `false` to indicate that it - * failed to find a complete token. - * - * Possible token types, based on the HTML specification: - * - * - an HTML tag, whether opening, closing, or void. - * - a text node - the plaintext inside tags. - * - an HTML comment. - * - a DOCTYPE declaration. - * - a processing instruction, e.g. ``. - * - * The Tag Processor currently only supports the tag token. - * - * @since 6.5.0 - * - * @return bool Whether a token was parsed. - */ - public function next_token() { - return $this->base_class_next_token(); - } - - /** - * Internal method which finds the next token in the HTML document. - * - * This method is a protected internal function which implements the logic for - * finding the next token in a document. It exists so that the parser can update - * its state without affecting the location of the cursor in the document and - * without triggering subclass methods for things like `next_token()`, e.g. when - * applying patches before searching for the next token. - * - * @since 6.5.0 - * - * @access private - * - * @return bool Whether a token was parsed. - */ - private function base_class_next_token() { - $was_at = $this->bytes_already_parsed; - $this->after_tag(); - - // Don't proceed if there's nothing more to scan. - if ( - self::STATE_COMPLETE === $this->parser_state || - self::STATE_INCOMPLETE_INPUT === $this->parser_state - ) { - return false; - } - - /* - * The next step in the parsing loop determines the parsing state; - * clear it so that state doesn't linger from the previous step. - */ - $this->parser_state = self::STATE_READY; - - if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { - $this->parser_state = self::STATE_COMPLETE; - return false; - } - - // Find the next tag if it exists. - if ( false === $this->parse_next_tag() ) { - if ( self::STATE_INCOMPLETE_INPUT === $this->parser_state ) { - $this->bytes_already_parsed = $was_at; - } - - return false; - } - - /* - * For legacy reasons the rest of this function handles tags and their - * attributes. If the processor has reached the end of the document - * or if it matched any other token then it should return here to avoid - * attempting to process tag-specific syntax. - */ - if ( - self::STATE_INCOMPLETE_INPUT !== $this->parser_state && - self::STATE_COMPLETE !== $this->parser_state && - self::STATE_MATCHED_TAG !== $this->parser_state - ) { - return true; - } - - // Parse all of its attributes. - while ( $this->parse_next_attribute() ) { - continue; - } - - // Ensure that the tag closes before the end of the document. - if ( - self::STATE_INCOMPLETE_INPUT === $this->parser_state || - $this->bytes_already_parsed >= strlen( $this->html ) - ) { - // Does this appropriately clear state (parsed attributes)? - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - $this->bytes_already_parsed = $was_at; - - return false; - } - - $tag_ends_at = strpos( $this->html, '>', $this->bytes_already_parsed ); - if ( false === $tag_ends_at ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - $this->bytes_already_parsed = $was_at; - - return false; - } - $this->parser_state = self::STATE_MATCHED_TAG; - $this->token_length = $tag_ends_at - $this->token_starts_at; - $this->bytes_already_parsed = $tag_ends_at + 1; - - /* - * For non-DATA sections which might contain text that looks like HTML tags but - * isn't, scan with the appropriate alternative mode. Looking at the first letter - * of the tag name as a pre-check avoids a string allocation when it's not needed. - */ - $t = $this->html[ $this->tag_name_starts_at ]; - if ( - $this->is_closing_tag || - ! ( - 'i' === $t || 'I' === $t || - 'n' === $t || 'N' === $t || - 's' === $t || 'S' === $t || - 't' === $t || 'T' === $t || - 'x' === $t || 'X' === $t - ) - ) { - return true; - } - - $tag_name = $this->get_tag(); - - /* - * Preserve the opening tag pointers, as these will be overwritten - * when finding the closing tag. They will be reset after finding - * the closing to tag to point to the opening of the special atomic - * tag sequence. - */ - $tag_name_starts_at = $this->tag_name_starts_at; - $tag_name_length = $this->tag_name_length; - $tag_ends_at = $this->token_starts_at + $this->token_length; - $attributes = $this->attributes; - $duplicate_attributes = $this->duplicate_attributes; - - // Find the closing tag if necessary. - $found_closer = false; - switch ( $tag_name ) { - case 'SCRIPT': - $found_closer = $this->skip_script_data(); - break; - - case 'TEXTAREA': - case 'TITLE': - $found_closer = $this->skip_rcdata( $tag_name ); - break; - - /* - * In the browser this list would include the NOSCRIPT element, - * but the Tag Processor is an environment with the scripting - * flag disabled, meaning that it needs to descend into the - * NOSCRIPT element to be able to properly process what will be - * sent to a browser. - * - * Note that this rule makes HTML5 syntax incompatible with XML, - * because the parsing of this token depends on client application. - * The NOSCRIPT element cannot be represented in the XHTML syntax. - */ - case 'IFRAME': - case 'NOEMBED': - case 'NOFRAMES': - case 'STYLE': - case 'XMP': - $found_closer = $this->skip_rawtext( $tag_name ); - break; - - // No other tags should be treated in their entirety here. - default: - return true; - } - - if ( ! $found_closer ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - $this->bytes_already_parsed = $was_at; - return false; - } - - /* - * The values here look like they reference the opening tag but they reference - * the closing tag instead. This is why the opening tag values were stored - * above in a variable. It reads confusingly here, but that's because the - * functions that skip the contents have moved all the internal cursors past - * the inner content of the tag. - */ - $this->token_starts_at = $was_at; - $this->token_length = $this->bytes_already_parsed - $this->token_starts_at; - $this->text_starts_at = $tag_ends_at + 1; - $this->text_length = $this->tag_name_starts_at - $this->text_starts_at; - $this->tag_name_starts_at = $tag_name_starts_at; - $this->tag_name_length = $tag_name_length; - $this->attributes = $attributes; - $this->duplicate_attributes = $duplicate_attributes; - - return true; - } - - /** - * Whether the processor paused because the input HTML document ended - * in the middle of a syntax element, such as in the middle of a tag. - * - * Example: - * - * $processor = new WP_HTML_Tag_Processor( '" ); - * $p->next_tag(); - * foreach ( $p->class_list() as $class_name ) { - * echo "{$class_name} "; - * } - * // Outputs: "free lang-en " - * - * @since 6.4.0 - */ - public function class_list() { - if ( self::STATE_MATCHED_TAG !== $this->parser_state ) { - return; - } - - /** @var string $class contains the string value of the class attribute, with character references decoded. */ - $class = $this->get_attribute( 'class' ); - - if ( ! is_string( $class ) ) { - return; - } - - $seen = array(); - - $at = 0; - while ( $at < strlen( $class ) ) { - // Skip past any initial boundary characters. - $at += strspn( $class, " \t\f\r\n", $at ); - if ( $at >= strlen( $class ) ) { - return; - } - - // Find the byte length until the next boundary. - $length = strcspn( $class, " \t\f\r\n", $at ); - if ( 0 === $length ) { - return; - } - - /* - * CSS class names are case-insensitive in the ASCII range. - * - * @see https://www.w3.org/TR/CSS2/syndata.html#x1 - */ - $name = strtolower( substr( $class, $at, $length ) ); - $at += $length; - - /* - * It's expected that the number of class names for a given tag is relatively small. - * Given this, it is probably faster overall to scan an array for a value rather - * than to use the class name as a key and check if it's a key of $seen. - */ - if ( in_array( $name, $seen, true ) ) { - continue; - } - - $seen[] = $name; - yield $name; - } - } - - - /** - * Returns if a matched tag contains the given ASCII case-insensitive class name. - * - * @since 6.4.0 - * - * @param string $wanted_class Look for this CSS class name, ASCII case-insensitive. - * @return bool|null Whether the matched tag contains the given class name, or null if not matched. - */ - public function has_class( $wanted_class ) { - if ( self::STATE_MATCHED_TAG !== $this->parser_state ) { - return null; - } - - $wanted_class = strtolower( $wanted_class ); - - foreach ( $this->class_list() as $class_name ) { - if ( $class_name === $wanted_class ) { - return true; - } - } - - return false; - } - - - /** - * Sets a bookmark in the HTML document. - * - * Bookmarks represent specific places or tokens in the HTML - * document, such as a tag opener or closer. When applying - * edits to a document, such as setting an attribute, the - * text offsets of that token may shift; the bookmark is - * kept updated with those shifts and remains stable unless - * the entire span of text in which the token sits is removed. - * - * Release bookmarks when they are no longer needed. - * - * Example: - * - *

Surprising fact you may not know!

- * ^ ^ - * \-|-- this `H2` opener bookmark tracks the token - * - *

Surprising fact you may no… - * ^ ^ - * \-|-- it shifts with edits - * - * Bookmarks provide the ability to seek to a previously-scanned - * place in the HTML document. This avoids the need to re-scan - * the entire document. - * - * Example: - * - *
  • One
  • Two
  • Three
- * ^^^^ - * want to note this last item - * - * $p = new WP_HTML_Tag_Processor( $html ); - * $in_list = false; - * while ( $p->next_tag( array( 'tag_closers' => $in_list ? 'visit' : 'skip' ) ) ) { - * if ( 'UL' === $p->get_tag() ) { - * if ( $p->is_tag_closer() ) { - * $in_list = false; - * $p->set_bookmark( 'resume' ); - * if ( $p->seek( 'last-li' ) ) { - * $p->add_class( 'last-li' ); - * } - * $p->seek( 'resume' ); - * $p->release_bookmark( 'last-li' ); - * $p->release_bookmark( 'resume' ); - * } else { - * $in_list = true; - * } - * } - * - * if ( 'LI' === $p->get_tag() ) { - * $p->set_bookmark( 'last-li' ); - * } - * } - * - * Bookmarks intentionally hide the internal string offsets - * to which they refer. They are maintained internally as - * updates are applied to the HTML document and therefore - * retain their "position" - the location to which they - * originally pointed. The inability to use bookmarks with - * functions like `substr` is therefore intentional to guard - * against accidentally breaking the HTML. - * - * Because bookmarks allocate memory and require processing - * for every applied update, they are limited and require - * a name. They should not be created with programmatically-made - * names, such as "li_{$index}" with some loop. As a general - * rule they should only be created with string-literal names - * like "start-of-section" or "last-paragraph". - * - * Bookmarks are a powerful tool to enable complicated behavior. - * Consider double-checking that you need this tool if you are - * reaching for it, as inappropriate use could lead to broken - * HTML structure or unwanted processing overhead. - * - * @since 6.2.0 - * - * @param string $name Identifies this particular bookmark. - * @return bool Whether the bookmark was successfully created. - */ - public function set_bookmark( $name ) { - // It only makes sense to set a bookmark if the parser has paused on a concrete token. - if ( - self::STATE_COMPLETE === $this->parser_state || - self::STATE_INCOMPLETE_INPUT === $this->parser_state - ) { - return false; - } - - if ( ! array_key_exists( $name, $this->bookmarks ) && count( $this->bookmarks ) >= static::MAX_BOOKMARKS ) { - _doing_it_wrong( - __METHOD__, - __( 'Too many bookmarks: cannot create any more.' ), - '6.2.0' - ); - return false; - } - - $this->bookmarks[ $name ] = new Gutenberg_HTML_Span_6_5( $this->token_starts_at, $this->token_length ); - - return true; - } - - - /** - * Removes a bookmark that is no longer needed. - * - * Releasing a bookmark frees up the small - * performance overhead it requires. - * - * @param string $name Name of the bookmark to remove. - * @return bool Whether the bookmark already existed before removal. - */ - public function release_bookmark( $name ) { - if ( ! array_key_exists( $name, $this->bookmarks ) ) { - return false; - } - - unset( $this->bookmarks[ $name ] ); - - return true; - } - - /** - * Skips contents of generic rawtext elements. - * - * @since 6.3.2 - * - * @see https://html.spec.whatwg.org/#generic-raw-text-element-parsing-algorithm - * - * @param string $tag_name The uppercase tag name which will close the RAWTEXT region. - * @return bool Whether an end to the RAWTEXT region was found before the end of the document. - */ - private function skip_rawtext( $tag_name ) { - /* - * These two functions distinguish themselves on whether character references are - * decoded, and since functionality to read the inner markup isn't supported, it's - * not necessary to implement these two functions separately. - */ - return $this->skip_rcdata( $tag_name ); - } - - /** - * Skips contents of RCDATA elements, namely title and textarea tags. - * - * @since 6.2.0 - * - * @see https://html.spec.whatwg.org/multipage/parsing.html#rcdata-state - * - * @param string $tag_name The uppercase tag name which will close the RCDATA region. - * @return bool Whether an end to the RCDATA region was found before the end of the document. - */ - private function skip_rcdata( $tag_name ) { - $html = $this->html; - $doc_length = strlen( $html ); - $tag_length = strlen( $tag_name ); - - $at = $this->bytes_already_parsed; - - while ( false !== $at && $at < $doc_length ) { - $at = strpos( $this->html, 'tag_name_starts_at = $at; - - // Fail if there is no possible tag closer. - if ( false === $at || ( $at + $tag_length ) >= $doc_length ) { - return false; - } - - $at += 2; - - /* - * Find a case-insensitive match to the tag name. - * - * Because tag names are limited to US-ASCII there is no - * need to perform any kind of Unicode normalization when - * comparing; any character which could be impacted by such - * normalization could not be part of a tag name. - */ - for ( $i = 0; $i < $tag_length; $i++ ) { - $tag_char = $tag_name[ $i ]; - $html_char = $html[ $at + $i ]; - - if ( $html_char !== $tag_char && strtoupper( $html_char ) !== $tag_char ) { - $at += $i; - continue 2; - } - } - - $at += $tag_length; - $this->bytes_already_parsed = $at; - - if ( $at >= strlen( $html ) ) { - return false; - } - - /* - * Ensure that the tag name terminates to avoid matching on - * substrings of a longer tag name. For example, the sequence - * "' !== $c ) { - continue; - } - - while ( $this->parse_next_attribute() ) { - continue; - } - - $at = $this->bytes_already_parsed; - if ( $at >= strlen( $this->html ) ) { - return false; - } - - if ( '>' === $html[ $at ] ) { - $this->bytes_already_parsed = $at + 1; - return true; - } - - if ( $at + 1 >= strlen( $this->html ) ) { - return false; - } - - if ( '/' === $html[ $at ] && '>' === $html[ $at + 1 ] ) { - $this->bytes_already_parsed = $at + 2; - return true; - } - } - - return false; - } - - /** - * Skips contents of script tags. - * - * @since 6.2.0 - * - * @return bool Whether the script tag was closed before the end of the document. - */ - private function skip_script_data() { - $state = 'unescaped'; - $html = $this->html; - $doc_length = strlen( $html ); - $at = $this->bytes_already_parsed; - - while ( false !== $at && $at < $doc_length ) { - $at += strcspn( $html, '-<', $at ); - - /* - * For all script states a "-->" transitions - * back into the normal unescaped script mode, - * even if that's the current state. - */ - if ( - $at + 2 < $doc_length && - '-' === $html[ $at ] && - '-' === $html[ $at + 1 ] && - '>' === $html[ $at + 2 ] - ) { - $at += 3; - $state = 'unescaped'; - continue; - } - - // Everything of interest past here starts with "<". - if ( $at + 1 >= $doc_length || '<' !== $html[ $at++ ] ) { - continue; - } - - /* - * Unlike with "-->", the "`. Unlike other comment - * and bogus comment syntax, these leave no clear insertion point for text and - * they need to be modified specially in order to contain text. E.g. to store - * `?` as the modifiable text, the `` needs to become ``, which - * involves inserting an additional `-` into the token after the modifiable text. - */ - $this->parser_state = self::STATE_COMMENT; - $this->comment_type = self::COMMENT_AS_ABRUPTLY_CLOSED_COMMENT; - $this->token_length = $closer_at + $span_of_dashes + 1 - $this->token_starts_at; - - // Only provide modifiable text if the token is long enough to contain it. - if ( $span_of_dashes >= 2 ) { - $this->comment_type = self::COMMENT_AS_HTML_COMMENT; - $this->text_starts_at = $this->token_starts_at + 4; - $this->text_length = $span_of_dashes - 2; - } - - $this->bytes_already_parsed = $closer_at + $span_of_dashes + 1; - return true; - } - - /* - * Comments may be closed by either a --> or an invalid --!>. - * The first occurrence closes the comment. - * - * See https://html.spec.whatwg.org/#parse-error-incorrectly-closed-comment - */ - --$closer_at; // Pre-increment inside condition below reduces risk of accidental infinite looping. - while ( ++$closer_at < $doc_length ) { - $closer_at = strpos( $html, '--', $closer_at ); - if ( false === $closer_at ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - - return false; - } - - if ( $closer_at + 2 < $doc_length && '>' === $html[ $closer_at + 2 ] ) { - $this->parser_state = self::STATE_COMMENT; - $this->comment_type = self::COMMENT_AS_HTML_COMMENT; - $this->token_length = $closer_at + 3 - $this->token_starts_at; - $this->text_starts_at = $this->token_starts_at + 4; - $this->text_length = $closer_at - $this->text_starts_at; - $this->bytes_already_parsed = $closer_at + 3; - return true; - } - - if ( - $closer_at + 3 < $doc_length && - '!' === $html[ $closer_at + 2 ] && - '>' === $html[ $closer_at + 3 ] - ) { - $this->parser_state = self::STATE_COMMENT; - $this->comment_type = self::COMMENT_AS_HTML_COMMENT; - $this->token_length = $closer_at + 4 - $this->token_starts_at; - $this->text_starts_at = $this->token_starts_at + 4; - $this->text_length = $closer_at - $this->text_starts_at; - $this->bytes_already_parsed = $closer_at + 4; - return true; - } - } - } - - /* - * ` - * These are ASCII-case-insensitive. - * https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state - */ - if ( - $doc_length > $at + 8 && - ( 'D' === $html[ $at + 2 ] || 'd' === $html[ $at + 2 ] ) && - ( 'O' === $html[ $at + 3 ] || 'o' === $html[ $at + 3 ] ) && - ( 'C' === $html[ $at + 4 ] || 'c' === $html[ $at + 4 ] ) && - ( 'T' === $html[ $at + 5 ] || 't' === $html[ $at + 5 ] ) && - ( 'Y' === $html[ $at + 6 ] || 'y' === $html[ $at + 6 ] ) && - ( 'P' === $html[ $at + 7 ] || 'p' === $html[ $at + 7 ] ) && - ( 'E' === $html[ $at + 8 ] || 'e' === $html[ $at + 8 ] ) - ) { - $closer_at = strpos( $html, '>', $at + 9 ); - if ( false === $closer_at ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - - return false; - } - - $this->parser_state = self::STATE_DOCTYPE; - $this->token_length = $closer_at + 1 - $this->token_starts_at; - $this->text_starts_at = $this->token_starts_at + 9; - $this->text_length = $closer_at - $this->text_starts_at; - $this->bytes_already_parsed = $closer_at + 1; - return true; - } - - /* - * Anything else here is an incorrectly-opened comment and transitions - * to the bogus comment state - skip to the nearest >. If no closer is - * found then the HTML was truncated inside the markup declaration. - */ - $closer_at = strpos( $html, '>', $at + 1 ); - if ( false === $closer_at ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - - return false; - } - - $this->parser_state = self::STATE_COMMENT; - $this->comment_type = self::COMMENT_AS_INVALID_HTML; - $this->token_length = $closer_at + 1 - $this->token_starts_at; - $this->text_starts_at = $this->token_starts_at + 2; - $this->text_length = $closer_at - $this->text_starts_at; - $this->bytes_already_parsed = $closer_at + 1; - - /* - * Identify nodes that would be CDATA if HTML had CDATA sections. - * - * This section must occur after identifying the bogus comment end - * because in an HTML parser it will span to the nearest `>`, even - * if there's no `]]>` as would be required in an XML document. It - * is therefore not possible to parse a CDATA section containing - * a `>` in the HTML syntax. - * - * Inside foreign elements there is a discrepancy between browsers - * and the specification on this. - * - * @todo Track whether the Tag Processor is inside a foreign element - * and require the proper closing `]]>` in those cases. - */ - if ( - $this->token_length >= 10 && - '[' === $html[ $this->token_starts_at + 2 ] && - 'C' === $html[ $this->token_starts_at + 3 ] && - 'D' === $html[ $this->token_starts_at + 4 ] && - 'A' === $html[ $this->token_starts_at + 5 ] && - 'T' === $html[ $this->token_starts_at + 6 ] && - 'A' === $html[ $this->token_starts_at + 7 ] && - '[' === $html[ $this->token_starts_at + 8 ] && - ']' === $html[ $closer_at - 1 ] && - ']' === $html[ $closer_at - 2 ] - ) { - $this->parser_state = self::STATE_COMMENT; - $this->comment_type = self::COMMENT_AS_CDATA_LOOKALIKE; - $this->text_starts_at += 7; - $this->text_length -= 9; - } - - return true; - } - - /* - * is a missing end tag name, which is ignored. - * - * This was also known as the "presumptuous empty tag" - * in early discussions as it was proposed to close - * the nearest previous opening tag. - * - * See https://html.spec.whatwg.org/#parse-error-missing-end-tag-name - */ - if ( '>' === $html[ $at + 1 ] ) { - $this->parser_state = self::STATE_PRESUMPTUOUS_TAG; - $this->token_length = $at + 2 - $this->token_starts_at; - $this->bytes_already_parsed = $at + 2; - return true; - } - - /* - * ` - * See https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state - */ - if ( '?' === $html[ $at + 1 ] ) { - $closer_at = strpos( $html, '>', $at + 2 ); - if ( false === $closer_at ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - - return false; - } - - $this->parser_state = self::STATE_COMMENT; - $this->comment_type = self::COMMENT_AS_INVALID_HTML; - $this->token_length = $closer_at + 1 - $this->token_starts_at; - $this->text_starts_at = $this->token_starts_at + 2; - $this->text_length = $closer_at - $this->text_starts_at; - $this->bytes_already_parsed = $closer_at + 1; - - /* - * Identify a Processing Instruction node were HTML to have them. - * - * This section must occur after identifying the bogus comment end - * because in an HTML parser it will span to the nearest `>`, even - * if there's no `?>` as would be required in an XML document. It - * is therefore not possible to parse a Processing Instruction node - * containing a `>` in the HTML syntax. - * - * XML allows for more target names, but this code only identifies - * those with ASCII-representable target names. This means that it - * may identify some Processing Instruction nodes as bogus comments, - * but it will not misinterpret the HTML structure. By limiting the - * identification to these target names the Tag Processor can avoid - * the need to start parsing UTF-8 sequences. - * - * > NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | - * [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | - * [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | - * [#x10000-#xEFFFF] - * > NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040] - * - * @see https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-PITarget - */ - if ( $this->token_length >= 5 && '?' === $html[ $closer_at - 1 ] ) { - $comment_text = substr( $html, $this->token_starts_at + 2, $this->token_length - 4 ); - $pi_target_length = strspn( $comment_text, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:_' ); - - if ( 0 < $pi_target_length ) { - $pi_target_length += strspn( $comment_text, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789:_-.', $pi_target_length ); - - $this->comment_type = self::COMMENT_AS_PI_NODE_LOOKALIKE; - $this->tag_name_starts_at = $this->token_starts_at + 2; - $this->tag_name_length = $pi_target_length; - $this->text_starts_at += $pi_target_length; - $this->text_length -= $pi_target_length + 1; - } - } - - return true; - } - - /* - * If a non-alpha starts the tag name in a tag closer it's a comment. - * Find the first `>`, which closes the comment. - * - * This parser classifies these particular comments as special "funky comments" - * which are made available for further processing. - * - * See https://html.spec.whatwg.org/#parse-error-invalid-first-character-of-tag-name - */ - if ( $this->is_closing_tag ) { - // No chance of finding a closer. - if ( $at + 3 > $doc_length ) { - return false; - } - - $closer_at = strpos( $html, '>', $at + 3 ); - if ( false === $closer_at ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - - return false; - } - - $this->parser_state = self::STATE_FUNKY_COMMENT; - $this->token_length = $closer_at + 1 - $this->token_starts_at; - $this->text_starts_at = $this->token_starts_at + 2; - $this->text_length = $closer_at - $this->text_starts_at; - $this->bytes_already_parsed = $closer_at + 1; - return true; - } - - ++$at; - } - - return false; - } - - /** - * Parses the next attribute. - * - * @since 6.2.0 - * - * @return bool Whether an attribute was found before the end of the document. - */ - private function parse_next_attribute() { - // Skip whitespace and slashes. - $this->bytes_already_parsed += strspn( $this->html, " \t\f\r\n/", $this->bytes_already_parsed ); - if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - - return false; - } - - /* - * Treat the equal sign as a part of the attribute - * name if it is the first encountered byte. - * - * @see https://html.spec.whatwg.org/multipage/parsing.html#before-attribute-name-state - */ - $name_length = '=' === $this->html[ $this->bytes_already_parsed ] - ? 1 + strcspn( $this->html, "=/> \t\f\r\n", $this->bytes_already_parsed + 1 ) - : strcspn( $this->html, "=/> \t\f\r\n", $this->bytes_already_parsed ); - - // No attribute, just tag closer. - if ( 0 === $name_length || $this->bytes_already_parsed + $name_length >= strlen( $this->html ) ) { - return false; - } - - $attribute_start = $this->bytes_already_parsed; - $attribute_name = substr( $this->html, $attribute_start, $name_length ); - $this->bytes_already_parsed += $name_length; - if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - - return false; - } - - $this->skip_whitespace(); - if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - - return false; - } - - $has_value = '=' === $this->html[ $this->bytes_already_parsed ]; - if ( $has_value ) { - ++$this->bytes_already_parsed; - $this->skip_whitespace(); - if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - - return false; - } - - switch ( $this->html[ $this->bytes_already_parsed ] ) { - case "'": - case '"': - $quote = $this->html[ $this->bytes_already_parsed ]; - $value_start = $this->bytes_already_parsed + 1; - $value_length = strcspn( $this->html, $quote, $value_start ); - $attribute_end = $value_start + $value_length + 1; - $this->bytes_already_parsed = $attribute_end; - break; - - default: - $value_start = $this->bytes_already_parsed; - $value_length = strcspn( $this->html, "> \t\f\r\n", $value_start ); - $attribute_end = $value_start + $value_length; - $this->bytes_already_parsed = $attribute_end; - } - } else { - $value_start = $this->bytes_already_parsed; - $value_length = 0; - $attribute_end = $attribute_start + $name_length; - } - - if ( $attribute_end >= strlen( $this->html ) ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - - return false; - } - - if ( $this->is_closing_tag ) { - return true; - } - - /* - * > There must never be two or more attributes on - * > the same start tag whose names are an ASCII - * > case-insensitive match for each other. - * - HTML 5 spec - * - * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive - */ - $comparable_name = strtolower( $attribute_name ); - - // If an attribute is listed many times, only use the first declaration and ignore the rest. - if ( ! array_key_exists( $comparable_name, $this->attributes ) ) { - $this->attributes[ $comparable_name ] = new Gutenberg_HTML_Attribute_Token_6_5( - $attribute_name, - $value_start, - $value_length, - $attribute_start, - $attribute_end - $attribute_start, - ! $has_value - ); - - return true; - } - - /* - * Track the duplicate attributes so if we remove it, all disappear together. - * - * While `$this->duplicated_attributes` could always be stored as an `array()`, - * which would simplify the logic here, storing a `null` and only allocating - * an array when encountering duplicates avoids needless allocations in the - * normative case of parsing tags with no duplicate attributes. - */ - $duplicate_span = new Gutenberg_HTML_Span_6_5( $attribute_start, $attribute_end - $attribute_start ); - if ( null === $this->duplicate_attributes ) { - $this->duplicate_attributes = array( $comparable_name => array( $duplicate_span ) ); - } elseif ( ! array_key_exists( $comparable_name, $this->duplicate_attributes ) ) { - $this->duplicate_attributes[ $comparable_name ] = array( $duplicate_span ); - } else { - $this->duplicate_attributes[ $comparable_name ][] = $duplicate_span; - } - - return true; - } - - /** - * Move the internal cursor past any immediate successive whitespace. - * - * @since 6.2.0 - */ - private function skip_whitespace() { - $this->bytes_already_parsed += strspn( $this->html, " \t\f\r\n", $this->bytes_already_parsed ); - } - - /** - * Applies attribute updates and cleans up once a tag is fully parsed. - * - * @since 6.2.0 - */ - private function after_tag() { - /* - * There could be lexical updates enqueued for an attribute that - * also exists on the next tag. In order to avoid conflating the - * attributes across the two tags, lexical updates with names - * need to be flushed to raw lexical updates. - */ - $this->class_name_updates_to_attributes_updates(); - - /* - * Purge updates if there are too many. The actual count isn't - * scientific, but a few values from 100 to a few thousand were - * tests to find a practially-useful limit. - * - * If the update queue grows too big, then the Tag Processor - * will spend more time iterating through them and lose the - * efficiency gains of deferring applying them. - */ - if ( 1000 < count( $this->lexical_updates ) ) { - $this->get_updated_html(); - } - - foreach ( $this->lexical_updates as $name => $update ) { - /* - * Any updates appearing after the cursor should be applied - * before proceeding, otherwise they may be overlooked. - */ - if ( $update->start >= $this->bytes_already_parsed ) { - $this->get_updated_html(); - break; - } - - if ( is_int( $name ) ) { - continue; - } - - $this->lexical_updates[] = $update; - unset( $this->lexical_updates[ $name ] ); - } - - $this->token_starts_at = null; - $this->token_length = null; - $this->tag_name_starts_at = null; - $this->tag_name_length = null; - $this->text_starts_at = 0; - $this->text_length = 0; - $this->is_closing_tag = null; - $this->attributes = array(); - $this->comment_type = null; - $this->duplicate_attributes = null; - } - - /** - * Converts class name updates into tag attributes updates - * (they are accumulated in different data formats for performance). - * - * @since 6.2.0 - * - * @see WP_HTML_Tag_Processor::$lexical_updates - * @see WP_HTML_Tag_Processor::$classname_updates - */ - private function class_name_updates_to_attributes_updates() { - if ( count( $this->classname_updates ) === 0 ) { - return; - } - - $existing_class = $this->get_enqueued_attribute_value( 'class' ); - if ( null === $existing_class || true === $existing_class ) { - $existing_class = ''; - } - - if ( false === $existing_class && isset( $this->attributes['class'] ) ) { - $existing_class = substr( - $this->html, - $this->attributes['class']->value_starts_at, - $this->attributes['class']->value_length - ); - } - - if ( false === $existing_class ) { - $existing_class = ''; - } - - /** - * Updated "class" attribute value. - * - * This is incrementally built while scanning through the existing class - * attribute, skipping removed classes on the way, and then appending - * added classes at the end. Only when finished processing will the - * value contain the final new value. - - * @var string $class - */ - $class = ''; - - /** - * Tracks the cursor position in the existing - * class attribute value while parsing. - * - * @var int $at - */ - $at = 0; - - /** - * Indicates if there's any need to modify the existing class attribute. - * - * If a call to `add_class()` and `remove_class()` wouldn't impact - * the `class` attribute value then there's no need to rebuild it. - * For example, when adding a class that's already present or - * removing one that isn't. - * - * This flag enables a performance optimization when none of the enqueued - * class updates would impact the `class` attribute; namely, that the - * processor can continue without modifying the input document, as if - * none of the `add_class()` or `remove_class()` calls had been made. - * - * This flag is set upon the first change that requires a string update. - * - * @var bool $modified - */ - $modified = false; - - // Remove unwanted classes by only copying the new ones. - $existing_class_length = strlen( $existing_class ); - while ( $at < $existing_class_length ) { - // Skip to the first non-whitespace character. - $ws_at = $at; - $ws_length = strspn( $existing_class, " \t\f\r\n", $ws_at ); - $at += $ws_length; - - // Capture the class name – it's everything until the next whitespace. - $name_length = strcspn( $existing_class, " \t\f\r\n", $at ); - if ( 0 === $name_length ) { - // If no more class names are found then that's the end. - break; - } - - $name = substr( $existing_class, $at, $name_length ); - $at += $name_length; - - // If this class is marked for removal, start processing the next one. - $remove_class = ( - isset( $this->classname_updates[ $name ] ) && - self::REMOVE_CLASS === $this->classname_updates[ $name ] - ); - - // If a class has already been seen then skip it; it should not be added twice. - if ( ! $remove_class ) { - $this->classname_updates[ $name ] = self::SKIP_CLASS; - } - - if ( $remove_class ) { - $modified = true; - continue; - } - - /* - * Otherwise, append it to the new "class" attribute value. - * - * There are options for handling whitespace between tags. - * Preserving the existing whitespace produces fewer changes - * to the HTML content and should clarify the before/after - * content when debugging the modified output. - * - * This approach contrasts normalizing the inter-class - * whitespace to a single space, which might appear cleaner - * in the output HTML but produce a noisier change. - */ - $class .= substr( $existing_class, $ws_at, $ws_length ); - $class .= $name; - } - - // Add new classes by appending those which haven't already been seen. - foreach ( $this->classname_updates as $name => $operation ) { - if ( self::ADD_CLASS === $operation ) { - $modified = true; - - $class .= strlen( $class ) > 0 ? ' ' : ''; - $class .= $name; - } - } - - $this->classname_updates = array(); - if ( ! $modified ) { - return; - } - - if ( strlen( $class ) > 0 ) { - $this->set_attribute( 'class', $class ); - } else { - $this->remove_attribute( 'class' ); - } - } - - /** - * Applies attribute updates to HTML document. - * - * @since 6.2.0 - * @since 6.2.1 Accumulates shift for internal cursor and passed pointer. - * @since 6.3.0 Invalidate any bookmarks whose targets are overwritten. - * - * @param int $shift_this_point Accumulate and return shift for this position. - * @return int How many bytes the given pointer moved in response to the updates. - */ - private function apply_attributes_updates( $shift_this_point = 0 ) { - if ( ! count( $this->lexical_updates ) ) { - return 0; - } - - $accumulated_shift_for_given_point = 0; - - /* - * Attribute updates can be enqueued in any order but updates - * to the document must occur in lexical order; that is, each - * replacement must be made before all others which follow it - * at later string indices in the input document. - * - * Sorting avoid making out-of-order replacements which - * can lead to mangled output, partially-duplicated - * attributes, and overwritten attributes. - */ - usort( $this->lexical_updates, array( self::class, 'sort_start_ascending' ) ); - - $bytes_already_copied = 0; - $output_buffer = ''; - foreach ( $this->lexical_updates as $diff ) { - $shift = strlen( $diff->text ) - $diff->length; - - // Adjust the cursor position by however much an update affects it. - if ( $diff->start < $this->bytes_already_parsed ) { - $this->bytes_already_parsed += $shift; - } - - // Accumulate shift of the given pointer within this function call. - if ( $diff->start <= $shift_this_point ) { - $accumulated_shift_for_given_point += $shift; - } - - $output_buffer .= substr( $this->html, $bytes_already_copied, $diff->start - $bytes_already_copied ); - $output_buffer .= $diff->text; - $bytes_already_copied = $diff->start + $diff->length; - } - - $this->html = $output_buffer . substr( $this->html, $bytes_already_copied ); - - /* - * Adjust bookmark locations to account for how the text - * replacements adjust offsets in the input document. - */ - foreach ( $this->bookmarks as $bookmark_name => $bookmark ) { - $bookmark_end = $bookmark->start + $bookmark->length; - - /* - * Each lexical update which appears before the bookmark's endpoints - * might shift the offsets for those endpoints. Loop through each change - * and accumulate the total shift for each bookmark, then apply that - * shift after tallying the full delta. - */ - $head_delta = 0; - $tail_delta = 0; - - foreach ( $this->lexical_updates as $diff ) { - $diff_end = $diff->start + $diff->length; - - if ( $bookmark->start < $diff->start && $bookmark_end < $diff->start ) { - break; - } - - if ( $bookmark->start >= $diff->start && $bookmark_end < $diff_end ) { - $this->release_bookmark( $bookmark_name ); - continue 2; - } - - $delta = strlen( $diff->text ) - $diff->length; - - if ( $bookmark->start >= $diff->start ) { - $head_delta += $delta; - } - - if ( $bookmark_end >= $diff_end ) { - $tail_delta += $delta; - } - } - - $bookmark->start += $head_delta; - $bookmark->length += $tail_delta - $head_delta; - } - - $this->lexical_updates = array(); - - return $accumulated_shift_for_given_point; - } - - /** - * Checks whether a bookmark with the given name exists. - * - * @since 6.3.0 - * - * @param string $bookmark_name Name to identify a bookmark that potentially exists. - * @return bool Whether that bookmark exists. - */ - public function has_bookmark( $bookmark_name ) { - return array_key_exists( $bookmark_name, $this->bookmarks ); - } - - /** - * Move the internal cursor in the Tag Processor to a given bookmark's location. - * - * In order to prevent accidental infinite loops, there's a - * maximum limit on the number of times seek() can be called. - * - * @since 6.2.0 - * - * @param string $bookmark_name Jump to the place in the document identified by this bookmark name. - * @return bool Whether the internal cursor was successfully moved to the bookmark's location. - */ - public function seek( $bookmark_name ) { - if ( ! array_key_exists( $bookmark_name, $this->bookmarks ) ) { - _doing_it_wrong( - __METHOD__, - __( 'Unknown bookmark name.' ), - '6.2.0' - ); - return false; - } - - if ( ++$this->seek_count > static::MAX_SEEK_OPS ) { - _doing_it_wrong( - __METHOD__, - __( 'Too many calls to seek() - this can lead to performance issues.' ), - '6.2.0' - ); - return false; - } - - // Flush out any pending updates to the document. - $this->get_updated_html(); - - // Point this tag processor before the sought tag opener and consume it. - $this->bytes_already_parsed = $this->bookmarks[ $bookmark_name ]->start; - $this->parser_state = self::STATE_READY; - return $this->next_token(); - } - - /** - * Compare two WP_HTML_Text_Replacement objects. - * - * @since 6.2.0 - * - * @param WP_HTML_Text_Replacement $a First attribute update. - * @param WP_HTML_Text_Replacement $b Second attribute update. - * @return int Comparison value for string order. - */ - private static function sort_start_ascending( $a, $b ) { - $by_start = $a->start - $b->start; - if ( 0 !== $by_start ) { - return $by_start; - } - - $by_text = isset( $a->text, $b->text ) ? strcmp( $a->text, $b->text ) : 0; - if ( 0 !== $by_text ) { - return $by_text; - } - - /* - * This code should be unreachable, because it implies the two replacements - * start at the same location and contain the same text. - */ - return $a->length - $b->length; - } - - /** - * Return the enqueued value for a given attribute, if one exists. - * - * Enqueued updates can take different data types: - * - If an update is enqueued and is boolean, the return will be `true` - * - If an update is otherwise enqueued, the return will be the string value of that update. - * - If an attribute is enqueued to be removed, the return will be `null` to indicate that. - * - If no updates are enqueued, the return will be `false` to differentiate from "removed." - * - * @since 6.2.0 - * - * @param string $comparable_name The attribute name in its comparable form. - * @return string|boolean|null Value of enqueued update if present, otherwise false. - */ - private function get_enqueued_attribute_value( $comparable_name ) { - if ( self::STATE_MATCHED_TAG !== $this->parser_state ) { - return false; - } - - if ( ! isset( $this->lexical_updates[ $comparable_name ] ) ) { - return false; - } - - $enqueued_text = $this->lexical_updates[ $comparable_name ]->text; - - // Removed attributes erase the entire span. - if ( '' === $enqueued_text ) { - return null; - } - - /* - * Boolean attribute updates are just the attribute name without a corresponding value. - * - * This value might differ from the given comparable name in that there could be leading - * or trailing whitespace, and that the casing follows the name given in `set_attribute`. - * - * Example: - * - * $p->set_attribute( 'data-TEST-id', 'update' ); - * 'update' === $p->get_enqueued_attribute_value( 'data-test-id' ); - * - * Detect this difference based on the absence of the `=`, which _must_ exist in any - * attribute containing a value, e.g. ``. - * ¹ ² - * 1. Attribute with a string value. - * 2. Boolean attribute whose value is `true`. - */ - $equals_at = strpos( $enqueued_text, '=' ); - if ( false === $equals_at ) { - return true; - } - - /* - * Finally, a normal update's value will appear after the `=` and - * be double-quoted, as performed incidentally by `set_attribute`. - * - * e.g. `type="text"` - * ¹² ³ - * 1. Equals is here. - * 2. Double-quoting starts one after the equals sign. - * 3. Double-quoting ends at the last character in the update. - */ - $enqueued_value = substr( $enqueued_text, $equals_at + 2, -1 ); - return html_entity_decode( $enqueued_value ); - } - - /** - * Returns the value of a requested attribute from a matched tag opener if that attribute exists. - * - * Example: - * - * $p = new WP_HTML_Tag_Processor( '
Test
' ); - * $p->next_tag( array( 'class_name' => 'test' ) ) === true; - * $p->get_attribute( 'data-test-id' ) === '14'; - * $p->get_attribute( 'enabled' ) === true; - * $p->get_attribute( 'aria-label' ) === null; - * - * $p->next_tag() === false; - * $p->get_attribute( 'class' ) === null; - * - * @since 6.2.0 - * - * @param string $name Name of attribute whose value is requested. - * @return string|true|null Value of attribute or `null` if not available. Boolean attributes return `true`. - */ - public function get_attribute( $name ) { - if ( self::STATE_MATCHED_TAG !== $this->parser_state ) { - return null; - } - - $comparable = strtolower( $name ); - - /* - * For every attribute other than `class` it's possible to perform a quick check if - * there's an enqueued lexical update whose value takes priority over what's found in - * the input document. - * - * The `class` attribute is special though because of the exposed helpers `add_class` - * and `remove_class`. These form a builder for the `class` attribute, so an additional - * check for enqueued class changes is required in addition to the check for any enqueued - * attribute values. If any exist, those enqueued class changes must first be flushed out - * into an attribute value update. - */ - if ( 'class' === $name ) { - $this->class_name_updates_to_attributes_updates(); - } - - // Return any enqueued attribute value updates if they exist. - $enqueued_value = $this->get_enqueued_attribute_value( $comparable ); - if ( false !== $enqueued_value ) { - return $enqueued_value; - } - - if ( ! isset( $this->attributes[ $comparable ] ) ) { - return null; - } - - $attribute = $this->attributes[ $comparable ]; - - /* - * This flag distinguishes an attribute with no value - * from an attribute with an empty string value. For - * unquoted attributes this could look very similar. - * It refers to whether an `=` follows the name. - * - * e.g.
- * ¹ ² - * 1. Attribute `boolean-attribute` is `true`. - * 2. Attribute `empty-attribute` is `""`. - */ - if ( true === $attribute->is_true ) { - return true; - } - - $raw_value = substr( $this->html, $attribute->value_starts_at, $attribute->value_length ); - - return html_entity_decode( $raw_value ); - } - - /** - * Gets lowercase names of all attributes matching a given prefix in the current tag. - * - * Note that matching is case-insensitive. This is in accordance with the spec: - * - * > There must never be two or more attributes on - * > the same start tag whose names are an ASCII - * > case-insensitive match for each other. - * - HTML 5 spec - * - * Example: - * - * $p = new WP_HTML_Tag_Processor( '
Test
' ); - * $p->next_tag( array( 'class_name' => 'test' ) ) === true; - * $p->get_attribute_names_with_prefix( 'data-' ) === array( 'data-enabled', 'data-test-id' ); - * - * $p->next_tag() === false; - * $p->get_attribute_names_with_prefix( 'data-' ) === null; - * - * @since 6.2.0 - * - * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive - * - * @param string $prefix Prefix of requested attribute names. - * @return array|null List of attribute names, or `null` when no tag opener is matched. - */ - public function get_attribute_names_with_prefix( $prefix ) { - if ( - self::STATE_MATCHED_TAG !== $this->parser_state || - $this->is_closing_tag - ) { - return null; - } - - $comparable = strtolower( $prefix ); - - $matches = array(); - foreach ( array_keys( $this->attributes ) as $attr_name ) { - if ( str_starts_with( $attr_name, $comparable ) ) { - $matches[] = $attr_name; - } - } - return $matches; - } - - /** - * Returns the uppercase name of the matched tag. - * - * Example: - * - * $p = new WP_HTML_Tag_Processor( '
Test
' ); - * $p->next_tag() === true; - * $p->get_tag() === 'DIV'; - * - * $p->next_tag() === false; - * $p->get_tag() === null; - * - * @since 6.2.0 - * - * @return string|null Name of currently matched tag in input HTML, or `null` if none found. - */ - public function get_tag() { - if ( null === $this->tag_name_starts_at ) { - return null; - } - - $tag_name = substr( $this->html, $this->tag_name_starts_at, $this->tag_name_length ); - - if ( self::STATE_MATCHED_TAG === $this->parser_state ) { - return strtoupper( $tag_name ); - } - - if ( - self::STATE_COMMENT === $this->parser_state && - self::COMMENT_AS_PI_NODE_LOOKALIKE === $this->get_comment_type() - ) { - return $tag_name; - } - - return null; - } - - /** - * Indicates if the currently matched tag contains the self-closing flag. - * - * No HTML elements ought to have the self-closing flag and for those, the self-closing - * flag will be ignored. For void elements this is benign because they "self close" - * automatically. For non-void HTML elements though problems will appear if someone - * intends to use a self-closing element in place of that element with an empty body. - * For HTML foreign elements and custom elements the self-closing flag determines if - * they self-close or not. - * - * This function does not determine if a tag is self-closing, - * but only if the self-closing flag is present in the syntax. - * - * @since 6.3.0 - * - * @return bool Whether the currently matched tag contains the self-closing flag. - */ - public function has_self_closing_flag() { - if ( self::STATE_MATCHED_TAG !== $this->parser_state ) { - return false; - } - - /* - * The self-closing flag is the solidus at the _end_ of the tag, not the beginning. - * - * Example: - * - *
- * ^ this appears one character before the end of the closing ">". - */ - return '/' === $this->html[ $this->token_starts_at + $this->token_length - 1 ]; - } - - /** - * Indicates if the current tag token is a tag closer. - * - * Example: - * - * $p = new WP_HTML_Tag_Processor( '
' ); - * $p->next_tag( array( 'tag_name' => 'div', 'tag_closers' => 'visit' ) ); - * $p->is_tag_closer() === false; - * - * $p->next_tag( array( 'tag_name' => 'div', 'tag_closers' => 'visit' ) ); - * $p->is_tag_closer() === true; - * - * @since 6.2.0 - * - * @return bool Whether the current tag is a tag closer. - */ - public function is_tag_closer() { - return ( - self::STATE_MATCHED_TAG === $this->parser_state && - $this->is_closing_tag - ); - } - - /** - * Indicates the kind of matched token, if any. - * - * This differs from `get_token_name()` in that it always - * returns a static string indicating the type, whereas - * `get_token_name()` may return values derived from the - * token itself, such as a tag name or processing - * instruction tag. - * - * Possible values: - * - `#tag` when matched on a tag. - * - `#text` when matched on a text node. - * - `#cdata-section` when matched on a CDATA node. - * - `#comment` when matched on a comment. - * - `#doctype` when matched on a DOCTYPE declaration. - * - `#presumptuous-tag` when matched on an empty tag closer. - * - `#funky-comment` when matched on a funky comment. - * - * @since 6.5.0 - * - * @return string|null What kind of token is matched, or null. - */ - public function get_token_type() { - switch ( $this->parser_state ) { - case self::STATE_MATCHED_TAG: - return '#tag'; - - case self::STATE_DOCTYPE: - return '#doctype'; - - default: - return $this->get_token_name(); - } - } - - /** - * Returns the node name represented by the token. - * - * This matches the DOM API value `nodeName`. Some values - * are static, such as `#text` for a text node, while others - * are dynamically generated from the token itself. - * - * Dynamic names: - * - Uppercase tag name for tag matches. - * - `html` for DOCTYPE declarations. - * - * Note that if the Tag Processor is not matched on a token - * then this function will return `null`, either because it - * hasn't yet found a token or because it reached the end - * of the document without matching a token. - * - * @since 6.5.0 - * - * @return string|null Name of the matched token. - */ - public function get_token_name() { - switch ( $this->parser_state ) { - case self::STATE_MATCHED_TAG: - return $this->get_tag(); - - case self::STATE_TEXT_NODE: - return '#text'; - - case self::STATE_CDATA_NODE: - return '#cdata-section'; - - case self::STATE_COMMENT: - return '#comment'; - - case self::STATE_DOCTYPE: - return 'html'; - - case self::STATE_PRESUMPTUOUS_TAG: - return '#presumptuous-tag'; - - case self::STATE_FUNKY_COMMENT: - return '#funky-comment'; - } - } - - /** - * Indicates what kind of comment produced the comment node. - * - * Because there are different kinds of HTML syntax which produce - * comments, the Tag Processor tracks and exposes this as a type - * for the comment. Nominally only regular HTML comments exist as - * they are commonly known, but a number of unrelated syntax errors - * also produce comments. - * - * @see self::COMMENT_AS_ABRUPTLY_CLOSED_COMMENT - * @see self::COMMENT_AS_CDATA_LOOKALIKE - * @see self::COMMENT_AS_INVALID_HTML - * @see self::COMMENT_AS_HTML_COMMENT - * @see self::COMMENT_AS_PI_NODE_LOOKALIKE - * - * @since 6.5.0 - * - * @return string|null - */ - public function get_comment_type() { - if ( self::STATE_COMMENT !== $this->parser_state ) { - return null; - } - - return $this->comment_type; - } - - /** - * Returns the modifiable text for a matched token, or an empty string. - * - * Modifiable text is text content that may be read and changed without - * changing the HTML structure of the document around it. This includes - * the contents of `#text` nodes in the HTML as well as the inner - * contents of HTML comments, Processing Instructions, and others, even - * though these nodes aren't part of a parsed DOM tree. They also contain - * the contents of SCRIPT and STYLE tags, of TEXTAREA tags, and of any - * other section in an HTML document which cannot contain HTML markup (DATA). - * - * If a token has no modifiable text then an empty string is returned to - * avoid needless crashing or type errors. An empty string does not mean - * that a token has modifiable text, and a token with modifiable text may - * have an empty string (e.g. a comment with no contents). - * - * @since 6.5.0 - * - * @return string - */ - public function get_modifiable_text() { - if ( null === $this->text_starts_at ) { - return ''; - } - - $text = substr( $this->html, $this->text_starts_at, $this->text_length ); - - // Comment data is not decoded. - if ( - self::STATE_CDATA_NODE === $this->parser_state || - self::STATE_COMMENT === $this->parser_state || - self::STATE_DOCTYPE === $this->parser_state || - self::STATE_FUNKY_COMMENT === $this->parser_state - ) { - return $text; - } - - $tag_name = $this->get_tag(); - if ( - // Script data is not decoded. - 'SCRIPT' === $tag_name || - - // RAWTEXT data is not decoded. - 'IFRAME' === $tag_name || - 'NOEMBED' === $tag_name || - 'NOFRAMES' === $tag_name || - 'STYLE' === $tag_name || - 'XMP' === $tag_name - ) { - return $text; - } - - $decoded = html_entity_decode( $text, ENT_QUOTES | ENT_HTML5 | ENT_SUBSTITUTE ); - - /* - * TEXTAREA skips a leading newline, but this newline may appear not only as the - * literal character `\n`, but also as a character reference, such as in the - * following markup: ``. - * - * For these cases it's important to first decode the text content before checking - * for a leading newline and removing it. - */ - if ( - self::STATE_MATCHED_TAG === $this->parser_state && - 'TEXTAREA' === $tag_name && - strlen( $decoded ) > 0 && - "\n" === $decoded[0] - ) { - return substr( $decoded, 1 ); - } - - return $decoded; - } - - /** - * Updates or creates a new attribute on the currently matched tag with the passed value. - * - * For boolean attributes special handling is provided: - * - When `true` is passed as the value, then only the attribute name is added to the tag. - * - When `false` is passed, the attribute gets removed if it existed before. - * - * For string attributes, the value is escaped using the `esc_attr` function. - * - * @since 6.2.0 - * @since 6.2.1 Fix: Only create a single update for multiple calls with case-variant attribute names. - * - * @param string $name The attribute name to target. - * @param string|bool $value The new attribute value. - * @return bool Whether an attribute value was set. - */ - public function set_attribute( $name, $value ) { - if ( - self::STATE_MATCHED_TAG !== $this->parser_state || - $this->is_closing_tag - ) { - return false; - } - - /* - * WordPress rejects more characters than are strictly forbidden - * in HTML5. This is to prevent additional security risks deeper - * in the WordPress and plugin stack. Specifically the - * less-than (<) greater-than (>) and ampersand (&) aren't allowed. - * - * The use of a PCRE match enables looking for specific Unicode - * code points without writing a UTF-8 decoder. Whereas scanning - * for one-byte characters is trivial (with `strcspn`), scanning - * for the longer byte sequences would be more complicated. Given - * that this shouldn't be in the hot path for execution, it's a - * reasonable compromise in efficiency without introducing a - * noticeable impact on the overall system. - * - * @see https://html.spec.whatwg.org/#attributes-2 - * - * @todo As the only regex pattern maybe we should take it out? - * Are Unicode patterns available broadly in Core? - */ - if ( preg_match( - '~[' . - // Syntax-like characters. - '"\'>& The values "true" and "false" are not allowed on boolean attributes. - * > To represent a false value, the attribute has to be omitted altogether. - * - HTML5 spec, https://html.spec.whatwg.org/#boolean-attributes - */ - if ( false === $value ) { - return $this->remove_attribute( $name ); - } - - if ( true === $value ) { - $updated_attribute = $name; - } else { - $comparable_name = strtolower( $name ); - - /* - * Escape URL attributes. - * - * @see https://html.spec.whatwg.org/#attributes-3 - */ - $escaped_new_value = in_array( $comparable_name, wp_kses_uri_attributes() ) ? esc_url( $value ) : esc_attr( $value ); - $updated_attribute = "{$name}=\"{$escaped_new_value}\""; - } - - /* - * > There must never be two or more attributes on - * > the same start tag whose names are an ASCII - * > case-insensitive match for each other. - * - HTML 5 spec - * - * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive - */ - $comparable_name = strtolower( $name ); - - if ( isset( $this->attributes[ $comparable_name ] ) ) { - /* - * Update an existing attribute. - * - * Example – set attribute id to "new" in
: - * - *
- * ^-------------^ - * start end - * replacement: `id="new"` - * - * Result:
- */ - $existing_attribute = $this->attributes[ $comparable_name ]; - $this->lexical_updates[ $comparable_name ] = new Gutenberg_HTML_Text_Replacement_6_5( - $existing_attribute->start, - $existing_attribute->length, - $updated_attribute - ); - } else { - /* - * Create a new attribute at the tag's name end. - * - * Example – add attribute id="new" to
: - * - *
- * ^ - * start and end - * replacement: ` id="new"` - * - * Result:
- */ - $this->lexical_updates[ $comparable_name ] = new Gutenberg_HTML_Text_Replacement_6_5( - $this->tag_name_starts_at + $this->tag_name_length, - 0, - ' ' . $updated_attribute - ); - } - - /* - * Any calls to update the `class` attribute directly should wipe out any - * enqueued class changes from `add_class` and `remove_class`. - */ - if ( 'class' === $comparable_name && ! empty( $this->classname_updates ) ) { - $this->classname_updates = array(); - } - - return true; - } - - /** - * Remove an attribute from the currently-matched tag. - * - * @since 6.2.0 - * - * @param string $name The attribute name to remove. - * @return bool Whether an attribute was removed. - */ - public function remove_attribute( $name ) { - if ( - self::STATE_MATCHED_TAG !== $this->parser_state || - $this->is_closing_tag - ) { - return false; - } - - /* - * > There must never be two or more attributes on - * > the same start tag whose names are an ASCII - * > case-insensitive match for each other. - * - HTML 5 spec - * - * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive - */ - $name = strtolower( $name ); - - /* - * Any calls to update the `class` attribute directly should wipe out any - * enqueued class changes from `add_class` and `remove_class`. - */ - if ( 'class' === $name && count( $this->classname_updates ) !== 0 ) { - $this->classname_updates = array(); - } - - /* - * If updating an attribute that didn't exist in the input - * document, then remove the enqueued update and move on. - * - * For example, this might occur when calling `remove_attribute()` - * after calling `set_attribute()` for the same attribute - * and when that attribute wasn't originally present. - */ - if ( ! isset( $this->attributes[ $name ] ) ) { - if ( isset( $this->lexical_updates[ $name ] ) ) { - unset( $this->lexical_updates[ $name ] ); - } - return false; - } - - /* - * Removes an existing tag attribute. - * - * Example – remove the attribute id from
: - *
- * ^-------------^ - * start end - * replacement: `` - * - * Result:
- */ - $this->lexical_updates[ $name ] = new Gutenberg_HTML_Text_Replacement_6_5( - $this->attributes[ $name ]->start, - $this->attributes[ $name ]->length, - '' - ); - - // Removes any duplicated attributes if they were also present. - if ( null !== $this->duplicate_attributes && array_key_exists( $name, $this->duplicate_attributes ) ) { - foreach ( $this->duplicate_attributes[ $name ] as $attribute_token ) { - $this->lexical_updates[] = new Gutenberg_HTML_Text_Replacement_6_5( - $attribute_token->start, - $attribute_token->length, - '' - ); - } - } - - return true; - } - - /** - * Adds a new class name to the currently matched tag. - * - * @since 6.2.0 - * - * @param string $class_name The class name to add. - * @return bool Whether the class was set to be added. - */ - public function add_class( $class_name ) { - if ( - self::STATE_MATCHED_TAG !== $this->parser_state || - $this->is_closing_tag - ) { - return false; - } - - $this->classname_updates[ $class_name ] = self::ADD_CLASS; - - return true; - } - - /** - * Removes a class name from the currently matched tag. - * - * @since 6.2.0 - * - * @param string $class_name The class name to remove. - * @return bool Whether the class was set to be removed. - */ - public function remove_class( $class_name ) { - if ( - self::STATE_MATCHED_TAG !== $this->parser_state || - $this->is_closing_tag - ) { - return false; - } - - if ( null !== $this->tag_name_starts_at ) { - $this->classname_updates[ $class_name ] = self::REMOVE_CLASS; - } - - return true; - } - - /** - * Returns the string representation of the HTML Tag Processor. - * - * @since 6.2.0 - * - * @see WP_HTML_Tag_Processor::get_updated_html() - * - * @return string The processed HTML. - */ - public function __toString() { - return $this->get_updated_html(); - } - - /** - * Returns the string representation of the HTML Tag Processor. - * - * @since 6.2.0 - * @since 6.2.1 Shifts the internal cursor corresponding to the applied updates. - * @since 6.4.0 No longer calls subclass method `next_tag()` after updating HTML. - * - * @return string The processed HTML. - */ - public function get_updated_html() { - $requires_no_updating = 0 === count( $this->classname_updates ) && 0 === count( $this->lexical_updates ); - - /* - * When there is nothing more to update and nothing has already been - * updated, return the original document and avoid a string copy. - */ - if ( $requires_no_updating ) { - return $this->html; - } - - /* - * Keep track of the position right before the current tag. This will - * be necessary for reparsing the current tag after updating the HTML. - */ - $before_current_tag = $this->token_starts_at; - - /* - * 1. Apply the enqueued edits and update all the pointers to reflect those changes. - */ - $this->class_name_updates_to_attributes_updates(); - $before_current_tag += $this->apply_attributes_updates( $before_current_tag ); - - /* - * 2. Rewind to before the current tag and reparse to get updated attributes. - * - * At this point the internal cursor points to the end of the tag name. - * Rewind before the tag name starts so that it's as if the cursor didn't - * move; a call to `next_tag()` will reparse the recently-updated attributes - * and additional calls to modify the attributes will apply at this same - * location, but in order to avoid issues with subclasses that might add - * behaviors to `next_tag()`, the internal methods should be called here - * instead. - * - * It's important to note that in this specific place there will be no change - * because the processor was already at a tag when this was called and it's - * rewinding only to the beginning of this very tag before reprocessing it - * and its attributes. - * - *

Previous HTMLMore HTML

- * ↑ │ back up by the length of the tag name plus the opening < - * └←─┘ back up by strlen("em") + 1 ==> 3 - */ - $this->bytes_already_parsed = $before_current_tag; - $this->base_class_next_token(); - - return $this->html; - } - - /** - * Parses tag query input into internal search criteria. - * - * @since 6.2.0 - * - * @param array|string|null $query { - * Optional. Which tag name to find, having which class, etc. Default is to find any tag. - * - * @type string|null $tag_name Which tag to find, or `null` for "any tag." - * @type int|null $match_offset Find the Nth tag matching all search criteria. - * 1 for "first" tag, 3 for "third," etc. - * Defaults to first tag. - * @type string|null $class_name Tag must contain this class name to match. - * @type string $tag_closers "visit" or "skip": whether to stop on tag closers, e.g.
. - * } - */ - private function parse_query( $query ) { - if ( null !== $query && $query === $this->last_query ) { - return; - } - - $this->last_query = $query; - $this->sought_tag_name = null; - $this->sought_class_name = null; - $this->sought_match_offset = 1; - $this->stop_on_tag_closers = false; - - // A single string value means "find the tag of this name". - if ( is_string( $query ) ) { - $this->sought_tag_name = $query; - return; - } - - // An empty query parameter applies no restrictions on the search. - if ( null === $query ) { - return; - } - - // If not using the string interface, an associative array is required. - if ( ! is_array( $query ) ) { - _doing_it_wrong( - __METHOD__, - __( 'The query argument must be an array or a tag name.' ), - '6.2.0' - ); - return; - } - - if ( isset( $query['tag_name'] ) && is_string( $query['tag_name'] ) ) { - $this->sought_tag_name = $query['tag_name']; - } - - if ( isset( $query['class_name'] ) && is_string( $query['class_name'] ) ) { - $this->sought_class_name = $query['class_name']; - } - - if ( isset( $query['match_offset'] ) && is_int( $query['match_offset'] ) && 0 < $query['match_offset'] ) { - $this->sought_match_offset = $query['match_offset']; - } - - if ( isset( $query['tag_closers'] ) ) { - $this->stop_on_tag_closers = 'visit' === $query['tag_closers']; - } - } - - - /** - * Checks whether a given tag and its attributes match the search criteria. - * - * @since 6.2.0 - * - * @return bool Whether the given tag and its attribute match the search criteria. - */ - private function matches() { - if ( $this->is_closing_tag && ! $this->stop_on_tag_closers ) { - return false; - } - - // Does the tag name match the requested tag name in a case-insensitive manner? - if ( null !== $this->sought_tag_name ) { - /* - * String (byte) length lookup is fast. If they aren't the - * same length then they can't be the same string values. - */ - if ( strlen( $this->sought_tag_name ) !== $this->tag_name_length ) { - return false; - } - - /* - * Check each character to determine if they are the same. - * Defer calls to `strtoupper()` to avoid them when possible. - * Calling `strcasecmp()` here tested slowed than comparing each - * character, so unless benchmarks show otherwise, it should - * not be used. - * - * It's expected that most of the time that this runs, a - * lower-case tag name will be supplied and the input will - * contain lower-case tag names, thus normally bypassing - * the case comparison code. - */ - for ( $i = 0; $i < $this->tag_name_length; $i++ ) { - $html_char = $this->html[ $this->tag_name_starts_at + $i ]; - $tag_char = $this->sought_tag_name[ $i ]; - - if ( $html_char !== $tag_char && strtoupper( $html_char ) !== $tag_char ) { - return false; - } - } - } - - if ( null !== $this->sought_class_name && ! $this->has_class( $this->sought_class_name ) ) { - return false; - } - - return true; - } - - /** - * Parser Ready State. - * - * Indicates that the parser is ready to run and waiting for a state transition. - * It may not have started yet, or it may have just finished parsing a token and - * is ready to find the next one. - * - * @since 6.5.0 - * - * @access private - */ - const STATE_READY = 'STATE_READY'; - - /** - * Parser Complete State. - * - * Indicates that the parser has reached the end of the document and there is - * nothing left to scan. It finished parsing the last token completely. - * - * @since 6.5.0 - * - * @access private - */ - const STATE_COMPLETE = 'STATE_COMPLETE'; - - /** - * Parser Incomplete Input State. - * - * Indicates that the parser has reached the end of the document before finishing - * a token. It started parsing a token but there is a possibility that the input - * HTML document was truncated in the middle of a token. - * - * The parser is reset at the start of the incomplete token and has paused. There - * is nothing more than can be scanned unless provided a more complete document. - * - * @since 6.5.0 - * - * @access private - */ - const STATE_INCOMPLETE_INPUT = 'STATE_INCOMPLETE_INPUT'; - - /** - * Parser Matched Tag State. - * - * Indicates that the parser has found an HTML tag and it's possible to get - * the tag name and read or modify its attributes (if it's not a closing tag). - * - * @since 6.5.0 - * - * @access private - */ - const STATE_MATCHED_TAG = 'STATE_MATCHED_TAG'; - - /** - * Parser Text Node State. - * - * Indicates that the parser has found a text node and it's possible - * to read and modify that text. - * - * @since 6.5.0 - * - * @access private - */ - const STATE_TEXT_NODE = 'STATE_TEXT_NODE'; - - /** - * Parser CDATA Node State. - * - * Indicates that the parser has found a CDATA node and it's possible - * to read and modify its modifiable text. Note that in HTML there are - * no CDATA nodes outside of foreign content (SVG and MathML). Outside - * of foreign content, they are treated as HTML comments. - * - * @since 6.5.0 - * - * @access private - */ - const STATE_CDATA_NODE = 'STATE_CDATA_NODE'; - - /** - * Indicates that the parser has found an HTML comment and it's - * possible to read and modify its modifiable text. - * - * @since 6.5.0 - * - * @access private - */ - const STATE_COMMENT = 'STATE_COMMENT'; - - /** - * Indicates that the parser has found a DOCTYPE node and it's - * possible to read and modify its modifiable text. - * - * @since 6.5.0 - * - * @access private - */ - const STATE_DOCTYPE = 'STATE_DOCTYPE'; - - /** - * Indicates that the parser has found an empty tag closer ``. - * - * Note that in HTML there are no empty tag closers, and they - * are ignored. Nonetheless, the Tag Processor still - * recognizes them as they appear in the HTML stream. - * - * These were historically discussed as a "presumptuous tag - * closer," which would close the nearest open tag, but were - * dismissed in favor of explicitly-closing tags. - * - * @since 6.5.0 - * - * @access private - */ - const STATE_PRESUMPTUOUS_TAG = 'STATE_PRESUMPTUOUS_TAG'; - - /** - * Indicates that the parser has found a "funky comment" - * and it's possible to read and modify its modifiable text. - * - * Example: - * - * - * - * - * - * Funky comments are tag closers with invalid tag names. Note - * that in HTML these are turn into bogus comments. Nonetheless, - * the Tag Processor recognizes them in a stream of HTML and - * exposes them for inspection and modification. - * - * @since 6.5.0 - * - * @access private - */ - const STATE_FUNKY_COMMENT = 'STATE_WP_FUNKY'; - - /** - * Indicates that a comment was created when encountering abruptly-closed HTML comment. - * - * Example: - * - * - * - * - * @since 6.5.0 - */ - const COMMENT_AS_ABRUPTLY_CLOSED_COMMENT = 'COMMENT_AS_ABRUPTLY_CLOSED_COMMENT'; - - /** - * Indicates that a comment would be parsed as a CDATA node, - * were HTML to allow CDATA nodes outside of foreign content. - * - * Example: - * - * - * - * This is an HTML comment, but it looks like a CDATA node. - * - * @since 6.5.0 - */ - const COMMENT_AS_CDATA_LOOKALIKE = 'COMMENT_AS_CDATA_LOOKALIKE'; - - /** - * Indicates that a comment was created when encountering - * normative HTML comment syntax. - * - * Example: - * - * - * - * @since 6.5.0 - */ - const COMMENT_AS_HTML_COMMENT = 'COMMENT_AS_HTML_COMMENT'; - - /** - * Indicates that a comment would be parsed as a Processing - * Instruction node, were they to exist within HTML. - * - * Example: - * - * - * - * This is an HTML comment, but it looks like a CDATA node. - * - * @since 6.5.0 - */ - const COMMENT_AS_PI_NODE_LOOKALIKE = 'COMMENT_AS_PI_NODE_LOOKALIKE'; - - /** - * Indicates that a comment was created when encountering invalid - * HTML input, a so-called "bogus comment." - * - * Example: - * - * - * - * - * @since 6.5.0 - */ - const COMMENT_AS_INVALID_HTML = 'COMMENT_AS_INVALID_HTML'; -} diff --git a/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-text-replacement-6-5.php b/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-text-replacement-6-5.php deleted file mode 100644 index 6409255833c81..0000000000000 --- a/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-text-replacement-6-5.php +++ /dev/null @@ -1,64 +0,0 @@ -start = $start; - $this->length = $length; - $this->text = $text; - } -} diff --git a/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api-directives-processor.php b/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api-directives-processor.php deleted file mode 100644 index ba18307002d15..0000000000000 --- a/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api-directives-processor.php +++ /dev/null @@ -1,246 +0,0 @@ -get_tag() ) { - return null; - } - - $positions = $this->get_after_opener_tag_and_before_closer_tag_positions(); - if ( ! $positions ) { - return null; - } - list( $after_opener_tag, $before_closer_tag ) = $positions; - - return substr( $this->html, $after_opener_tag, $before_closer_tag - $after_opener_tag ); - } - - /** - * Sets the content between two balanced tags. - * - * @since 6.5.0 - * - * @access private - * - * @param string $new_content The string to replace the content between the matching tags. - * @return bool Whether the content was successfully replaced. - */ - public function set_content_between_balanced_tags( string $new_content ): bool { - $positions = $this->get_after_opener_tag_and_before_closer_tag_positions( true ); - if ( ! $positions ) { - return false; - } - list( $after_opener_tag, $before_closer_tag ) = $positions; - - $this->lexical_updates[] = new Gutenberg_HTML_Text_Replacement_6_5( - $after_opener_tag, - $before_closer_tag - $after_opener_tag, - esc_html( $new_content ) - ); - - return true; - } - - /** - * Appends content after the closing tag of a template tag. - * - * It positions the cursor in the closer tag of the balanced template tag, - * if it exists. - * - * @access private - * - * @param string $new_content The string to append after the closing template tag. - * @return bool Whether the content was successfully appended. - */ - public function append_content_after_template_tag_closer( string $new_content ): bool { - if ( empty( $new_content ) || 'TEMPLATE' !== $this->get_tag() || ! $this->is_tag_closer() ) { - return false; - } - - // Flushes any changes. - $this->get_updated_html(); - - $bookmark = 'append_content_after_template_tag_closer'; - $this->set_bookmark( $bookmark ); - $after_closing_tag = $this->bookmarks[ $bookmark ]->start + $this->bookmarks[ $bookmark ]->length + 1; - $this->release_bookmark( $bookmark ); - - // Appends the new content. - $this->lexical_updates[] = new Gutenberg_HTML_Text_Replacement_6_5( $after_closing_tag, 0, $new_content ); - - return true; - } - - /** - * Gets the positions right after the opener tag and right before the closer - * tag in a balanced tag. - * - * By default, it positions the cursor in the closer tag of the balanced tag. - * If $rewind is true, it seeks back to the opener tag. - * - * @since 6.5.0 - * - * @access private - * - * @param bool $rewind Optional. Whether to seek back to the opener tag after finding the positions. Defaults to false. - * @return array|null Start and end byte position, or null when no balanced tag bookmarks. - */ - private function get_after_opener_tag_and_before_closer_tag_positions( bool $rewind = false ) { - // Flushes any changes. - $this->get_updated_html(); - - $bookmarks = $this->get_balanced_tag_bookmarks(); - if ( ! $bookmarks ) { - return null; - } - list( $opener_tag, $closer_tag ) = $bookmarks; - - $after_opener_tag = $this->bookmarks[ $opener_tag ]->start + $this->bookmarks[ $opener_tag ]->length + 1; - $before_closer_tag = $this->bookmarks[ $closer_tag ]->start; - - if ( $rewind ) { - $this->seek( $opener_tag ); - } - - $this->release_bookmark( $opener_tag ); - $this->release_bookmark( $closer_tag ); - - return array( $after_opener_tag, $before_closer_tag ); - } - - /** - * Returns a pair of bookmarks for the current opener tag and the matching - * closer tag. - * - * It positions the cursor in the closer tag of the balanced tag, if it - * exists. - * - * @since 6.5.0 - * - * @return array|null A pair of bookmarks, or null if there's no matching closing tag. - */ - private function get_balanced_tag_bookmarks() { - static $i = 0; - $opener_tag = 'opener_tag_of_balanced_tag_' . ++$i; - - $this->set_bookmark( $opener_tag ); - if ( ! $this->next_balanced_tag_closer_tag() ) { - $this->release_bookmark( $opener_tag ); - return null; - } - - $closer_tag = 'closer_tag_of_balanced_tag_' . ++$i; - $this->set_bookmark( $closer_tag ); - - return array( $opener_tag, $closer_tag ); - } - - /** - * Finds the matching closing tag for an opening tag. - * - * When called while the processor is on an open tag, it traverses the HTML - * until it finds the matching closer tag, respecting any in-between content, - * including nested tags of the same name. Returns false when called on a - * closer tag, a tag that doesn't have a closer tag (void), a tag that - * doesn't visit the closer tag, or if no matching closing tag was found. - * - * @since 6.5.0 - * - * @access private - * - * @return bool Whether a matching closing tag was found. - */ - public function next_balanced_tag_closer_tag(): bool { - $depth = 0; - $tag_name = $this->get_tag(); - - if ( ! $this->has_and_visits_its_closer_tag() ) { - return false; - } - - while ( $this->next_tag( - array( - 'tag_name' => $tag_name, - 'tag_closers' => 'visit', - ) - ) ) { - if ( ! $this->is_tag_closer() ) { - ++$depth; - continue; - } - - if ( 0 === $depth ) { - return true; - } - - --$depth; - } - - return false; - } - - /** - * Checks whether the current tag has and will visit its matching closer tag. - * - * @since 6.5.0 - * - * @access private - * - * @return bool Whether the current tag has a closer tag. - */ - public function has_and_visits_its_closer_tag(): bool { - $tag_name = $this->get_tag(); - - return null !== $tag_name && ( - ! Gutenberg_HTML_Processor_6_5::is_void( $tag_name ) && - ! in_array( $tag_name, self::TAGS_THAT_DONT_VISIT_CLOSER_TAG, true ) - ); - } - } -} diff --git a/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php b/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php deleted file mode 100644 index d0661d7585726..0000000000000 --- a/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php +++ /dev/null @@ -1,992 +0,0 @@ - 'data_wp_interactive_processor', - 'data-wp-router-region' => 'data_wp_router_region_processor', - 'data-wp-context' => 'data_wp_context_processor', - 'data-wp-bind' => 'data_wp_bind_processor', - 'data-wp-class' => 'data_wp_class_processor', - 'data-wp-style' => 'data_wp_style_processor', - 'data-wp-text' => 'data_wp_text_processor', - /* - * `data-wp-each` needs to be processed in the last place because it moves - * the cursor to the end of the processed items to prevent them to be - * processed twice. - */ - 'data-wp-each' => 'data_wp_each_processor', - ); - - /** - * Holds the initial state of the different Interactivity API stores. - * - * This state is used during the server directive processing. Then, it is - * serialized and sent to the client as part of the interactivity data to be - * recovered during the hydration of the client interactivity stores. - * - * @since 6.5.0 - * @var array - */ - private $state_data = array(); - - /** - * Holds the configuration required by the different Interactivity API stores. - * - * This configuration is serialized and sent to the client as part of the - * interactivity data and can be accessed by the client interactivity stores. - * - * @since 6.5.0 - * @var array - */ - private $config_data = array(); - - /** - * Flag that indicates whether the `data-wp-router-region` directive has - * been found in the HTML and processed. - * - * The value is saved in a private property of the WP_Interactivity_API - * instance instead of using a static variable inside the processor - * function, which would hold the same value for all instances - * independently of whether they have processed any - * `data-wp-router-region` directive or not. - * - * @since 6.5.0 - * @var bool - */ - private $has_processed_router_region = false; - - /** - * Gets and/or sets the initial state of an Interactivity API store for a - * given namespace. - * - * If state for that store namespace already exists, it merges the new - * provided state with the existing one. - * - * @since 6.5.0 - * - * @param string $store_namespace The unique store namespace identifier. - * @param array $state Optional. The array that will be merged with the existing state for the specified - * store namespace. - * @return array The current state for the specified store namespace. This will be the updated state if a $state - * argument was provided. - */ - public function state( string $store_namespace, array $state = array() ): array { - if ( ! isset( $this->state_data[ $store_namespace ] ) ) { - $this->state_data[ $store_namespace ] = array(); - } - if ( is_array( $state ) ) { - $this->state_data[ $store_namespace ] = array_replace_recursive( - $this->state_data[ $store_namespace ], - $state - ); - } - return $this->state_data[ $store_namespace ]; - } - - /** - * Gets and/or sets the configuration of the Interactivity API for a given - * store namespace. - * - * If configuration for that store namespace exists, it merges the new - * provided configuration with the existing one. - * - * @since 6.5.0 - * - * @param string $store_namespace The unique store namespace identifier. - * @param array $config Optional. The array that will be merged with the existing configuration for the - * specified store namespace. - * @return array The configuration for the specified store namespace. This will be the updated configuration if a - * $config argument was provided. - */ - public function config( string $store_namespace, array $config = array() ): array { - if ( ! isset( $this->config_data[ $store_namespace ] ) ) { - $this->config_data[ $store_namespace ] = array(); - } - if ( is_array( $config ) ) { - $this->config_data[ $store_namespace ] = array_replace_recursive( - $this->config_data[ $store_namespace ], - $config - ); - } - return $this->config_data[ $store_namespace ]; - } - - /** - * Prints the serialized client-side interactivity data. - * - * Encodes the config and initial state into JSON and prints them inside a - * script tag of type "application/json". Once in the browser, the state will - * be parsed and used to hydrate the client-side interactivity stores and the - * configuration will be available using a `getConfig` utility. - * - * @since 6.5.0 - */ - public function print_client_interactivity_data() { - if ( empty( $this->state_data ) && empty( $this->config_data ) ) { - return; - } - - $interactivity_data = array(); - - $config = array(); - foreach ( $this->config_data as $key => $value ) { - if ( ! empty( $value ) ) { - $config[ $key ] = $value; - } - } - if ( ! empty( $config ) ) { - $interactivity_data['config'] = $config; - } - - $state = array(); - foreach ( $this->state_data as $key => $value ) { - if ( ! empty( $value ) ) { - $state[ $key ] = $value; - } - } - if ( ! empty( $state ) ) { - $interactivity_data['state'] = $state; - } - - if ( ! empty( $interactivity_data ) ) { - wp_print_inline_script_tag( - wp_json_encode( - $interactivity_data, - JSON_HEX_TAG | JSON_HEX_AMP - ), - array( - 'type' => 'application/json', - 'id' => 'wp-interactivity-data', - ) - ); - } - } - - /** - * Registers the `@wordpress/interactivity` script modules. - * - * @since 6.5.0 - */ - public function register_script_modules() { - $suffix = wp_scripts_get_suffix(); - - wp_register_script_module( - '@wordpress/interactivity', - includes_url( "js/dist/interactivity$suffix.js" ) - ); - - wp_register_script_module( - '@wordpress/interactivity-router', - includes_url( "js/dist/interactivity-router$suffix.js" ), - array( '@wordpress/interactivity' ) - ); - } - - /** - * Adds the necessary hooks for the Interactivity API. - * - * @since 6.5.0 - */ - public function add_hooks() { - add_action( 'wp_enqueue_scripts', array( $this, 'register_script_modules' ) ); - add_action( 'wp_footer', array( $this, 'print_client_interactivity_data' ) ); - } - - /** - * Processes the interactivity directives contained within the HTML content - * and updates the markup accordingly. - * - * @since 6.5.0 - * - * @param string $html The HTML content to process. - * @return string The processed HTML content. It returns the original content when the HTML contains unbalanced tags. - */ - public function process_directives( string $html ): string { - if ( ! str_contains( $html, 'data-wp-' ) ) { - return $html; - } - $context_stack = array(); - $namespace_stack = array(); - $result = $this->process_directives_args( $html, $context_stack, $namespace_stack ); - return null === $result ? $html : $result; - } - - /** - * Processes the interactivity directives contained within the HTML content - * and updates the markup accordingly. - * - * It needs the context and namespace stacks to be passed by reference, and - * it returns null if the HTML contains unbalanced tags. - * - * @since 6.5.0 - * - * @param string $html The HTML content to process. - * @param array $context_stack The reference to the array used to keep track of contexts during processing. - * @param array $namespace_stack The reference to the array used to manage namespaces during processing. - * @return string|null The processed HTML content. It returns null when the HTML contains unbalanced tags. - */ - private function process_directives_args( string $html, array &$context_stack, array &$namespace_stack ) { - $p = new WP_Interactivity_API_Directives_Processor( $html ); - $tag_stack = array(); - $unbalanced = false; - - $directive_processor_prefixes = array_keys( self::$directive_processors ); - $directive_processor_prefixes_reversed = array_reverse( $directive_processor_prefixes ); - - while ( $p->next_tag( array( 'tag_closers' => 'visit' ) ) ) { - $tag_name = $p->get_tag(); - - if ( 'SVG' === $tag_name || 'MATH' === $tag_name ) { - $unbalanced = true; - break; - } - - if ( $p->is_tag_closer() ) { - list( $opening_tag_name, $directives_prefixes ) = end( $tag_stack ); - - if ( 0 === count( $tag_stack ) || $opening_tag_name !== $tag_name ) { - - /* - * If the tag stack is empty or the matching opening tag is not the - * same than the closing tag, it means the HTML is unbalanced and it - * stops processing it. - */ - $unbalanced = true; - break; - } else { - // Remove the last tag from the stack. - array_pop( $tag_stack ); - } - } else { - if ( 0 !== count( $p->get_attribute_names_with_prefix( 'data-wp-each-child' ) ) ) { - /* - * If the tag has a `data-wp-each-child` directive, jump to its closer - * tag because those tags have already been processed. - */ - $p->next_balanced_tag_closer_tag(); - continue; - } else { - $directives_prefixes = array(); - - // Checks if there is a server directive processor registered for each directive. - foreach ( $p->get_attribute_names_with_prefix( 'data-wp-' ) as $attribute_name ) { - list( $directive_prefix ) = $this->extract_prefix_and_suffix( $attribute_name ); - if ( array_key_exists( $directive_prefix, self::$directive_processors ) ) { - $directives_prefixes[] = $directive_prefix; - } - } - - /* - * If this tag will visit its closer tag, it adds it to the tag stack - * so it can process its closing tag and check for unbalanced tags. - */ - if ( $p->has_and_visits_its_closer_tag() ) { - $tag_stack[] = array( $tag_name, $directives_prefixes ); - } - } - } - /* - * If the matching opener tag didn't have any directives, it can skip the - * processing. - */ - if ( 0 === count( $directives_prefixes ) ) { - continue; - } - - // Directive processing might be different depending on if it is entering the tag or exiting it. - $modes = array( - 'enter' => ! $p->is_tag_closer(), - 'exit' => $p->is_tag_closer() || ! $p->has_and_visits_its_closer_tag(), - ); - foreach ( $modes as $mode => $should_run ) { - if ( ! $should_run ) { - continue; - } - - /* - * Sorts the attributes by the order of the `directives_processor` array - * and checks what directives are present in this element. - */ - $existing_directives_prefixes = array_intersect( - 'enter' === $mode ? $directive_processor_prefixes : $directive_processor_prefixes_reversed, - $directives_prefixes - ); - foreach ( $existing_directives_prefixes as $directive_prefix ) { - $func = is_array( self::$directive_processors[ $directive_prefix ] ) - ? self::$directive_processors[ $directive_prefix ] - : array( $this, self::$directive_processors[ $directive_prefix ] ); - - call_user_func_array( - $func, - array( $p, $mode, &$context_stack, &$namespace_stack, &$tag_stack ) - ); - } - } - } - - /* - * It returns null if the HTML is unbalanced because unbalanced HTML is - * not safe to process. In that case, the Interactivity API runtime will - * update the HTML on the client side during the hydration. - */ - return $unbalanced || 0 < count( $tag_stack ) ? null : $p->get_updated_html(); - } - - /** - * Evaluates the reference path passed to a directive based on the current - * store namespace, state and context. - * - * @since 6.5.0 - * - * @param string|true $directive_value The directive attribute value string or `true` when it's a boolean attribute. - * @param string $default_namespace The default namespace to use if none is explicitly defined in the directive - * value. - * @param array|false $context The current context for evaluating the directive or false if there is no - * context. - * @return mixed|null The result of the evaluation. Null if the reference path doesn't exist. - */ - private function evaluate( $directive_value, string $default_namespace, $context = false ) { - list( $ns, $path ) = $this->extract_directive_value( $directive_value, $default_namespace ); - if ( empty( $path ) ) { - return null; - } - - $store = array( - 'state' => $this->state_data[ $ns ] ?? array(), - 'context' => $context[ $ns ] ?? array(), - ); - - // Checks if the reference path is preceded by a negation operator (!). - $should_negate_value = '!' === $path[0]; - $path = $should_negate_value ? substr( $path, 1 ) : $path; - - // Extracts the value from the store using the reference path. - $path_segments = explode( '.', $path ); - $current = $store; - foreach ( $path_segments as $path_segment ) { - if ( isset( $current[ $path_segment ] ) ) { - $current = $current[ $path_segment ]; - } else { - return null; - } - } - - // Returns the opposite if it contains a negation operator (!). - return $should_negate_value ? ! $current : $current; - } - - /** - * Extracts the directive attribute name to separate and return the directive - * prefix and an optional suffix. - * - * The suffix is the string after the first double hyphen and the prefix is - * everything that comes before the suffix. - * - * Example: - * - * extract_prefix_and_suffix( 'data-wp-interactive' ) => array( 'data-wp-interactive', null ) - * extract_prefix_and_suffix( 'data-wp-bind--src' ) => array( 'data-wp-bind', 'src' ) - * extract_prefix_and_suffix( 'data-wp-foo--and--bar' ) => array( 'data-wp-foo', 'and--bar' ) - * - * @since 6.5.0 - * - * @param string $directive_name The directive attribute name. - * @return array An array containing the directive prefix and optional suffix. - */ - private function extract_prefix_and_suffix( string $directive_name ): array { - return explode( '--', $directive_name, 2 ); - } - - /** - * Parses and extracts the namespace and reference path from the given - * directive attribute value. - * - * If the value doesn't contain an explicit namespace, it returns the - * default one. If the value contains a JSON object instead of a reference - * path, the function tries to parse it and return the resulting array. If - * the value contains strings that represent booleans ("true" and "false"), - * numbers ("1" and "1.2") or "null", the function also transform them to - * regular booleans, numbers and `null`. - * - * Example: - * - * extract_directive_value( 'actions.foo', 'myPlugin' ) => array( 'myPlugin', 'actions.foo' ) - * extract_directive_value( 'otherPlugin::actions.foo', 'myPlugin' ) => array( 'otherPlugin', 'actions.foo' ) - * extract_directive_value( '{ "isOpen": false }', 'myPlugin' ) => array( 'myPlugin', array( 'isOpen' => false ) ) - * extract_directive_value( 'otherPlugin::{ "isOpen": false }', 'myPlugin' ) => array( 'otherPlugin', array( 'isOpen' => false ) ) - * - * @since 6.5.0 - * - * @param string|true $directive_value The directive attribute value. It can be `true` when it's a boolean - * attribute. - * @param string|null $default_namespace Optional. The default namespace if none is explicitly defined. - * @return array An array containing the namespace in the first item and the JSON, the reference path, or null on the - * second item. - */ - private function extract_directive_value( $directive_value, $default_namespace = null ): array { - if ( empty( $directive_value ) || is_bool( $directive_value ) ) { - return array( $default_namespace, null ); - } - - // Replaces the value and namespace if there is a namespace in the value. - if ( 1 === preg_match( '/^([\w\-_\/]+)::./', $directive_value ) ) { - list($default_namespace, $directive_value) = explode( '::', $directive_value, 2 ); - } - - /* - * Tries to decode the value as a JSON object. If it fails and the value - * isn't `null`, it returns the value as it is. Otherwise, it returns the - * decoded JSON or null for the string `null`. - */ - $decoded_json = json_decode( $directive_value, true ); - if ( null !== $decoded_json || 'null' === $directive_value ) { - $directive_value = $decoded_json; - } - - return array( $default_namespace, $directive_value ); - } - - /** - * Transforms a kebab-case string to camelCase. - * - * @param string $str The kebab-case string to transform to camelCase. - * @return string The transformed camelCase string. - */ - private function kebab_to_camel_case( string $str ): string { - return lcfirst( - preg_replace_callback( - '/(-)([a-z])/', - function ( $matches ) { - return strtoupper( $matches[2] ); - }, - strtolower( rtrim( $str, '-' ) ) - ) - ); - } - - /** - * Processes the `data-wp-interactive` directive. - * - * It adds the default store namespace defined in the directive value to the - * stack so that it's available for the nested interactivity elements. - * - * @since 6.5.0 - * - * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. - * @param string $mode Whether the processing is entering or exiting the tag. - * @param array $context_stack The reference to the context stack. - * @param array $namespace_stack The reference to the store namespace stack. - */ - private function data_wp_interactive_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { - // When exiting tags, it removes the last namespace from the stack. - if ( 'exit' === $mode ) { - array_pop( $namespace_stack ); - return; - } - - // Tries to decode the `data-wp-interactive` attribute value. - $attribute_value = $p->get_attribute( 'data-wp-interactive' ); - - /* - * Pushes the newly defined namespace or the current one if the - * `data-wp-interactive` definition was invalid or does not contain a - * namespace. It does so because the function pops out the current namespace - * from the stack whenever it finds a `data-wp-interactive`'s closing tag, - * independently of whether the previous `data-wp-interactive` definition - * contained a valid namespace. - */ - $new_namespace = null; - if ( is_string( $attribute_value ) && ! empty( $attribute_value ) ) { - $decoded_json = json_decode( $attribute_value, true ); - if ( is_array( $decoded_json ) ) { - $new_namespace = $decoded_json['namespace'] ?? null; - } else { - $new_namespace = $attribute_value; - } - } - $namespace_stack[] = ( $new_namespace && 1 === preg_match( '/^([\w\-_\/]+)/', $new_namespace ) ) - ? $new_namespace - : end( $namespace_stack ); - } - - /** - * Processes the `data-wp-context` directive. - * - * It adds the context defined in the directive value to the stack so that - * it's available for the nested interactivity elements. - * - * @since 6.5.0 - * - * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. - * @param string $mode Whether the processing is entering or exiting the tag. - * @param array $context_stack The reference to the context stack. - * @param array $namespace_stack The reference to the store namespace stack. - */ - private function data_wp_context_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { - // When exiting tags, it removes the last context from the stack. - if ( 'exit' === $mode ) { - array_pop( $context_stack ); - return; - } - - $attribute_value = $p->get_attribute( 'data-wp-context' ); - $namespace_value = end( $namespace_stack ); - - // Separates the namespace from the context JSON object. - list( $namespace_value, $decoded_json ) = is_string( $attribute_value ) && ! empty( $attribute_value ) - ? $this->extract_directive_value( $attribute_value, $namespace_value ) - : array( $namespace_value, null ); - - /* - * If there is a namespace, it adds a new context to the stack merging the - * previous context with the new one. - */ - if ( is_string( $namespace_value ) ) { - $context_stack[] = array_replace_recursive( - end( $context_stack ) !== false ? end( $context_stack ) : array(), - array( $namespace_value => is_array( $decoded_json ) ? $decoded_json : array() ) - ); - } else { - /* - * If there is no namespace, it pushes the current context to the stack. - * It needs to do so because the function pops out the current context - * from the stack whenever it finds a `data-wp-context`'s closing tag. - */ - $context_stack[] = end( $context_stack ); - } - } - - /** - * Processes the `data-wp-bind` directive. - * - * It updates or removes the bound attributes based on the evaluation of its - * associated reference. - * - * @since 6.5.0 - * - * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. - * @param string $mode Whether the processing is entering or exiting the tag. - * @param array $context_stack The reference to the context stack. - * @param array $namespace_stack The reference to the store namespace stack. - */ - private function data_wp_bind_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { - if ( 'enter' === $mode ) { - $all_bind_directives = $p->get_attribute_names_with_prefix( 'data-wp-bind--' ); - - foreach ( $all_bind_directives as $attribute_name ) { - list( , $bound_attribute ) = $this->extract_prefix_and_suffix( $attribute_name ); - if ( empty( $bound_attribute ) ) { - return; - } - - $attribute_value = $p->get_attribute( $attribute_name ); - $result = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) ); - - if ( null !== $result && ( - false !== $result || - ( strlen( $bound_attribute ) > 5 && '-' === $bound_attribute[4] ) - ) ) { - /* - * If the result of the evaluation is a boolean and the attribute is - * `aria-` or `data-, convert it to a string "true" or "false". It - * follows the exact same logic as Preact because it needs to - * replicate what Preact will later do in the client: - * https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L131C24-L136 - */ - if ( - is_bool( $result ) && - ( strlen( $bound_attribute ) > 5 && '-' === $bound_attribute[4] ) - ) { - $result = $result ? 'true' : 'false'; - } - $p->set_attribute( $bound_attribute, $result ); - } else { - $p->remove_attribute( $bound_attribute ); - } - } - } - } - - /** - * Processes the `data-wp-class` directive. - * - * It adds or removes CSS classes in the current HTML element based on the - * evaluation of its associated references. - * - * @since 6.5.0 - * - * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. - * @param string $mode Whether the processing is entering or exiting the tag. - * @param array $context_stack The reference to the context stack. - * @param array $namespace_stack The reference to the store namespace stack. - */ - private function data_wp_class_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { - if ( 'enter' === $mode ) { - $all_class_directives = $p->get_attribute_names_with_prefix( 'data-wp-class--' ); - - foreach ( $all_class_directives as $attribute_name ) { - list( , $class_name ) = $this->extract_prefix_and_suffix( $attribute_name ); - if ( empty( $class_name ) ) { - return; - } - - $attribute_value = $p->get_attribute( $attribute_name ); - $result = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) ); - - if ( $result ) { - $p->add_class( $class_name ); - } else { - $p->remove_class( $class_name ); - } - } - } - } - - /** - * Processes the `data-wp-style` directive. - * - * It updates the style attribute value of the current HTML element based on - * the evaluation of its associated references. - * - * @since 6.5.0 - * - * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. - * @param string $mode Whether the processing is entering or exiting the tag. - * @param array $context_stack The reference to the context stack. - * @param array $namespace_stack The reference to the store namespace stack. - */ - private function data_wp_style_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { - if ( 'enter' === $mode ) { - $all_style_attributes = $p->get_attribute_names_with_prefix( 'data-wp-style--' ); - - foreach ( $all_style_attributes as $attribute_name ) { - list( , $style_property ) = $this->extract_prefix_and_suffix( $attribute_name ); - if ( empty( $style_property ) ) { - continue; - } - - $directive_attribute_value = $p->get_attribute( $attribute_name ); - $style_property_value = $this->evaluate( $directive_attribute_value, end( $namespace_stack ), end( $context_stack ) ); - $style_attribute_value = $p->get_attribute( 'style' ); - $style_attribute_value = ( $style_attribute_value && ! is_bool( $style_attribute_value ) ) ? $style_attribute_value : ''; - - /* - * Checks first if the style property is not falsy and the style - * attribute value is not empty because if it is, it doesn't need to - * update the attribute value. - */ - if ( $style_property_value || $style_attribute_value ) { - $style_attribute_value = $this->merge_style_property( $style_attribute_value, $style_property, $style_property_value ); - /* - * If the style attribute value is not empty, it sets it. Otherwise, - * it removes it. - */ - if ( ! empty( $style_attribute_value ) ) { - $p->set_attribute( 'style', $style_attribute_value ); - } else { - $p->remove_attribute( 'style' ); - } - } - } - } - } - - /** - * Merges an individual style property in the `style` attribute of an HTML - * element, updating or removing the property when necessary. - * - * If a property is modified, the old one is removed and the new one is added - * at the end of the list. - * - * @since 6.5.0 - * - * Example: - * - * merge_style_property( 'color:green;', 'color', 'red' ) => 'color:red;' - * merge_style_property( 'background:green;', 'color', 'red' ) => 'background:green;color:red;' - * merge_style_property( 'color:green;', 'color', null ) => '' - * - * @param string $style_attribute_value The current style attribute value. - * @param string $style_property_name The style property name to set. - * @param string|false|null $style_property_value The value to set for the style property. With false, null or an - * empty string, it removes the style property. - * @return string The new style attribute value after the specified property has been added, updated or removed. - */ - private function merge_style_property( string $style_attribute_value, string $style_property_name, $style_property_value ): string { - $style_assignments = explode( ';', $style_attribute_value ); - $result = array(); - $style_property_value = ! empty( $style_property_value ) ? rtrim( trim( $style_property_value ), ';' ) : null; - $new_style_property = $style_property_value ? $style_property_name . ':' . $style_property_value . ';' : ''; - - // Generates an array with all the properties but the modified one. - foreach ( $style_assignments as $style_assignment ) { - if ( empty( trim( $style_assignment ) ) ) { - continue; - } - list( $name, $value ) = explode( ':', $style_assignment ); - if ( trim( $name ) !== $style_property_name ) { - $result[] = trim( $name ) . ':' . trim( $value ) . ';'; - } - } - - // Adds the new/modified property at the end of the list. - $result[] = $new_style_property; - - return implode( '', $result ); - } - - /** - * Processes the `data-wp-text` directive. - * - * It updates the inner content of the current HTML element based on the - * evaluation of its associated reference. - * - * @since 6.5.0 - * - * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. - * @param string $mode Whether the processing is entering or exiting the tag. - * @param array $context_stack The reference to the context stack. - * @param array $namespace_stack The reference to the store namespace stack. - */ - private function data_wp_text_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { - if ( 'enter' === $mode ) { - $attribute_value = $p->get_attribute( 'data-wp-text' ); - $result = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) ); - - /* - * Follows the same logic as Preact in the client and only changes the - * content if the value is a string or a number. Otherwise, it removes the - * content. - */ - if ( is_string( $result ) || is_numeric( $result ) ) { - $p->set_content_between_balanced_tags( esc_html( $result ) ); - } else { - $p->set_content_between_balanced_tags( '' ); - } - } - } - - /** - * Returns the CSS styles for animating the top loading bar in the router. - * - * @since 6.5.0 - * - * @return string The CSS styles for the router's top loading bar animation. - */ - private function get_router_animation_styles(): string { - return <<
-
-HTML; - } - - /** - * Processes the `data-wp-router-region` directive. - * - * It renders in the footer a set of HTML elements to notify users about - * client-side navigations. More concretely, the elements added are 1) a - * top loading bar to visually inform that a navigation is in progress - * and 2) an `aria-live` region for accessible navigation announcements. - * - * @since 6.5.0 - * - * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. - * @param string $mode Whether the processing is entering or exiting the tag. - */ - private function data_wp_router_region_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) { - if ( 'enter' === $mode && ! $this->has_processed_router_region ) { - $this->has_processed_router_region = true; - - // Initialize the `core/router` store. - $this->state( - 'core/router', - array( - 'navigation' => array( - 'texts' => array( - 'loading' => __( 'Loading page, please wait.' ), - 'loaded' => __( 'Page Loaded.' ), - ), - ), - ) - ); - - // Enqueues as an inline style. - wp_register_style( 'wp-interactivity-router-animations', false ); - wp_add_inline_style( 'wp-interactivity-router-animations', $this->get_router_animation_styles() ); - wp_enqueue_style( 'wp-interactivity-router-animations' ); - - // Adds the necessary markup to the footer. - add_action( 'wp_footer', array( $this, 'print_router_loading_and_screen_reader_markup' ) ); - } - } - - /** - * Processes the `data-wp-each` directive. - * - * This directive gets an array passed as reference and iterates over it - * generating new content for each item based on the inner markup of the - * `template` tag. - * - * @since 6.5.0 - * - * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. - * @param string $mode Whether the processing is entering or exiting the tag. - * @param array $context_stack The reference to the context stack. - * @param array $namespace_stack The reference to the store namespace stack. - * @param array $tag_stack The reference to the tag stack. - */ - private function data_wp_each_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack, array &$tag_stack ) { - if ( 'enter' === $mode && 'TEMPLATE' === $p->get_tag() ) { - $attribute_name = $p->get_attribute_names_with_prefix( 'data-wp-each' )[0]; - $extracted_suffix = $this->extract_prefix_and_suffix( $attribute_name ); - $item_name = isset( $extracted_suffix[1] ) ? $this->kebab_to_camel_case( $extracted_suffix[1] ) : 'item'; - $attribute_value = $p->get_attribute( $attribute_name ); - $result = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) ); - - // Gets the content between the template tags and leaves the cursor in the closer tag. - $inner_content = $p->get_content_between_balanced_template_tags(); - - // Checks if there is a manual server-side directive processing. - $template_end = 'data-wp-each: template end'; - $p->set_bookmark( $template_end ); - $p->next_tag(); - $manual_sdp = $p->get_attribute( 'data-wp-each-child' ); - $p->seek( $template_end ); // Rewinds to the template closer tag. - $p->release_bookmark( $template_end ); - - /* - * It doesn't process in these situations: - * - Manual server-side directive processing. - * - Empty or non-array values. - * - Associative arrays because those are deserialized as objects in JS. - * - Templates that contain top-level texts because those texts can't be - * identified and removed in the client. - */ - if ( - $manual_sdp || - empty( $result ) || - ! is_array( $result ) || - ! array_is_list( $result ) || - ! str_starts_with( trim( $inner_content ), '<' ) || - ! str_ends_with( trim( $inner_content ), '>' ) - ) { - array_pop( $tag_stack ); - return; - } - - // Extracts the namespace from the directive attribute value. - $namespace_value = end( $namespace_stack ); - list( $namespace_value ) = is_string( $attribute_value ) && ! empty( $attribute_value ) - ? $this->extract_directive_value( $attribute_value, $namespace_value ) - : array( $namespace_value, null ); - - // Processes the inner content for each item of the array. - $processed_content = ''; - foreach ( $result as $item ) { - // Creates a new context that includes the current item of the array. - $context_stack[] = array_replace_recursive( - end( $context_stack ) !== false ? end( $context_stack ) : array(), - array( $namespace_value => array( $item_name => $item ) ) - ); - - // Processes the inner content with the new context. - $processed_item = $this->process_directives_args( $inner_content, $context_stack, $namespace_stack ); - - if ( null === $processed_item ) { - // If the HTML is unbalanced, stop processing it. - array_pop( $context_stack ); - return; - } - - // Adds the `data-wp-each-child` to each top-level tag. - $i = new WP_Interactivity_API_Directives_Processor( $processed_item ); - while ( $i->next_tag() ) { - $i->set_attribute( 'data-wp-each-child', true ); - $i->next_balanced_tag_closer_tag(); - } - $processed_content .= $i->get_updated_html(); - - // Removes the current context from the stack. - array_pop( $context_stack ); - } - - // Appends the processed content after the tag closer of the template. - $p->append_content_after_template_tag_closer( $processed_content ); - - // Pops the last tag because it skipped the closing tag of the template tag. - array_pop( $tag_stack ); - } - } - } -} diff --git a/lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php b/lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php deleted file mode 100644 index 20e4365a48b6a..0000000000000 --- a/lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php +++ /dev/null @@ -1,205 +0,0 @@ -get_registered( $block_name ); - - if ( - isset( $block_name ) && - ( ( isset( $block_type->supports['interactivity'] ) && true === $block_type->supports['interactivity'] ) || - ( isset( $block_type->supports['interactivity']['interactive'] ) && true === $block_type->supports['interactivity']['interactive'] ) ) - ) { - // Annotates the root interactive block for processing. - $root_interactive_block = array( $block_name, $parsed_block ); - - /* - * Adds a filter to process the root interactive block once it has - * finished rendering. - */ - $process_interactive_blocks = static function ( string $content, array $parsed_block ) use ( &$root_interactive_block, &$process_interactive_blocks ): string { - // Checks whether the current block is the root interactive block. - list($root_block_name, $root_parsed_block) = $root_interactive_block; - if ( $root_block_name === $parsed_block['blockName'] && $parsed_block === $root_parsed_block ) { - // The root interactive blocks has finished rendering, process it. - $content = wp_interactivity_process_directives( $content ); - // Removes the filter and reset the root interactive block. - remove_filter( 'render_block_' . $parsed_block['blockName'], $process_interactive_blocks ); - $root_interactive_block = null; - } - return $content; - }; - - /* - * Uses a priority of 100 to ensure that other filters can add additional - * directives before the processing starts. - */ - add_filter( 'render_block_' . $block_name, $process_interactive_blocks, 100, 2 ); - } - } - - return $parsed_block; - } - /* - * Uses a priority of 100 to ensure that other filters can edit $parsed_block - * without crashing the SSR. - */ - add_filter( 'render_block_data', 'wp_interactivity_process_directives_of_interactive_blocks', 100 ); -} - -if ( ! function_exists( 'wp_interactivity' ) ) { - /** - * Retrieves the main WP_Interactivity_API instance. - * - * It provides access to the WP_Interactivity_API instance, creating one if it - * doesn't exist yet. - * - * @since 6.5.0 - * - * @global WP_Interactivity_API $wp_interactivity - * - * @return WP_Interactivity_API The main WP_Interactivity_API instance. - */ - function wp_interactivity(): WP_Interactivity_API { - global $wp_interactivity; - if ( ! ( $wp_interactivity instanceof WP_Interactivity_API ) ) { - $wp_interactivity = new WP_Interactivity_API(); - } - return $wp_interactivity; - } - - wp_interactivity()->add_hooks(); -} - -if ( ! function_exists( 'wp_interactivity_process_directives' ) ) { - /** - * Processes the interactivity directives contained within the HTML content - * and updates the markup accordingly. - * - * @since 6.5.0 - * - * @param string $html The HTML content to process. - * @return string The processed HTML content. It returns the original content when the HTML contains unbalanced tags. - */ - function wp_interactivity_process_directives( string $html ): string { - return wp_interactivity()->process_directives( $html ); - } -} - -if ( ! function_exists( 'wp_interactivity_state' ) ) { - /** - * Gets and/or sets the initial state of an Interactivity API store for a - * given namespace. - * - * If state for that store namespace already exists, it merges the new - * provided state with the existing one. - * - * @since 6.5.0 - * - * @param string $store_namespace The unique store namespace identifier. - * @param array $state Optional. The array that will be merged with the existing state for the specified - * store namespace. - * @return array The state for the specified store namespace. This will be the updated state if a $state argument was - * provided. - */ - function wp_interactivity_state( string $store_namespace, array $state = array() ): array { - return wp_interactivity()->state( $store_namespace, $state ); - } -} - -if ( ! function_exists( 'wp_interactivity_config' ) ) { - /** - * Gets and/or sets the configuration of the Interactivity API for a given - * store namespace. - * - * If configuration for that store namespace exists, it merges the new - * provided configuration with the existing one. - * - * @since 6.5.0 - * - * @param string $store_namespace The unique store namespace identifier. - * @param array $config Optional. The array that will be merged with the existing configuration for the - * specified store namespace. - * @return array The configuration for the specified store namespace. This will be the updated configuration if a - * $config argument was provided. - */ - function wp_interactivity_config( string $store_namespace, array $config = array() ): array { - return wp_interactivity()->config( $store_namespace, $config ); - } -} - -if ( ! function_exists( 'wp_interactivity_data_wp_context' ) ) { - /** - * Generates a `data-wp-context` directive attribute by encoding a context - * array. - * - * This helper function simplifies the creation of `data-wp-context` directives - * by providing a way to pass an array of data, which encodes into a JSON string - * safe for direct use as a HTML attribute value. - * - * Example: - * - *
true, 'count' => 0 ) ); ?>> - * - * @since 6.5.0 - * - * @param array $context The array of context data to encode. - * @param string $store_namespace Optional. The unique store namespace identifier. - * @return string A complete `data-wp-context` directive with a JSON encoded value representing the context array and - * the store namespace if specified. - */ - function wp_interactivity_data_wp_context( array $context, string $store_namespace = '' ): string { - return 'data-wp-context=\'' . - ( $store_namespace ? $store_namespace . '::' : '' ) . - ( empty( $context ) ? '{}' : wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ) . - '\''; - } -} - -if ( ! function_exists( 'data_wp_context' ) ) { - /** - * `data_wp_context()` was renamed to follow WordPress Core naming schemes. - * - * @link https://github.com/WordPress/gutenberg/pull/59465/ - * @link https://core.trac.wordpress.org/ticket/60575 - * - * @since 6.5.0 - * @deprecated 6.5.0 - * - * @param array $context The array of context data to encode. - * @param string $store_namespace Optional. The unique store namespace identifier. - * @return string A complete `data-wp-context` directive with a JSON encoded value representing the context array and - * the store namespace if specified. - */ - function data_wp_context( array $context, string $store_namespace = '' ): string { - _deprecated_function( __FUNCTION__, '6.5', 'wp_interactivity_data_wp_context()' ); - return wp_interactivity_data_wp_context( $context, $store_namespace ); - } -} diff --git a/lib/compat/wordpress-6.5/kses.php b/lib/compat/wordpress-6.5/kses.php deleted file mode 100644 index 038d78645786f..0000000000000 --- a/lib/compat/wordpress-6.5/kses.php +++ /dev/null @@ -1,18 +0,0 @@ -show_in_nav_menus ) { - $variation = build_variation_for_navigation_link( $post_type_object, 'post-type' ); - gutenberg_block_core_navigation_link_register_variation( $variation ); - } -} - -/** - * Registers a custom taxonomy variation for navigation link on taxonomy registration - * Handles all taxonomies registered after the block is registered in register_navigation_link_post_type_variations - * - * @since 6.5.0 - * @deprecated 6.5.0 Use WP_Block_Type::get_variations / get_block_type_variations filter instead. - * - * @param string $taxonomy Taxonomy slug. - * @param array|string $object_type Object type or array of object types. - * @param array $args Array of taxonomy registration arguments. - */ -function gutenberg_block_core_navigation_link_register_taxonomy_variation( $taxonomy, $object_type, $args ) { - _deprecated_function( __FUNCTION__, '6.5.0', 'WP_Block_Type::get_variations' ); - if ( isset( $args['show_in_nav_menus'] ) && $args['show_in_nav_menus'] ) { - $variation = build_variation_for_navigation_link( (object) $args, 'post-type' ); - gutenberg_block_core_navigation_link_register_variation( $variation ); - } -} - -/** - * Unregisters a custom post type variation for navigation link on post type unregistration. - * - * @since 6.5.0 - * @deprecated 6.5.0 Use WP_Block_Type::get_variations / get_block_type_variations filter instead. - * - * @param string $post_type The post type name passed from unregistered_post_type action hook. - */ -function gutenberg_block_core_navigation_link_unregister_post_type_variation( $post_type ) { - _deprecated_function( __FUNCTION__, '6.5.0', 'WP_Block_Type::get_variations' ); - gutenberg_block_core_navigation_link_unregister_variation( $post_type ); -} - -/** - * Unregisters a custom taxonomy variation for navigation link on taxonomy unregistration. - * - * @since 6.5.0 - * @deprecated 6.5.0 Use WP_Block_Type::get_variations / get_block_type_variations filter instead. - * - * @param string $taxonomy The taxonomy name passed from unregistered_taxonomy action hook. - */ -function gutenberg_block_core_navigation_link_unregister_taxonomy_variation( $taxonomy ) { - _deprecated_function( __FUNCTION__, '6.5.0', 'WP_Block_Type::get_variations' ); - gutenberg_block_core_navigation_link_unregister_variation( $taxonomy ); -} - -/** - * Registers a variation for a post type / taxonomy for the navigation link block. - * - * @since 6.5.0 - * @deprecated 6.5.0 Use WP_Block_Type::get_variations / get_block_type_variations filter instead. - * - * @param array $variation Variation array from build_variation_for_navigation_link. - */ -function gutenberg_block_core_navigation_link_register_variation( $variation ) { - _deprecated_function( __FUNCTION__, '6.5.0', 'WP_Block_Type::get_variations' ); - /* - * Directly set the variations on the registered block type - * because there's no server side registration for variations (see #47170). - */ - $navigation_block_type = WP_Block_Type_Registry::get_instance()->get_registered( 'core/navigation-link' ); - /* - * If the block is not registered yet, bail early. - * Variation will be registered in register_block_core_navigation_link then. - */ - if ( ! $navigation_block_type ) { - return; - } - - $navigation_block_type->variations = array_merge( - $navigation_block_type->variations, - array( $variation ) - ); -} - -/** - * Unregisters a variation for a post type / taxonomy for the navigation link block. - * - * @since 6.5.0 - * @deprecated 6.5.0 Use WP_Block_Type::get_variations / get_block_type_variations filter instead. - * - * @param string $name Name of the post type / taxonomy (which was used as variation name). - */ -function gutenberg_block_core_navigation_link_unregister_variation( $name ) { - _deprecated_function( __FUNCTION__, '6.5.0', 'WP_Block_Type::get_variations' ); - /* - * Directly get the variations from the registered block type - * because there's no server side (un)registration for variations (see #47170). - */ - $navigation_block_type = WP_Block_Type_Registry::get_instance()->get_registered( 'core/navigation-link' ); - // If the block is not registered (yet), there's no need to remove a variation. - if ( ! $navigation_block_type || empty( $navigation_block_type->variations ) ) { - return; - } - $variations = $navigation_block_type->variations; - // Search for the variation and remove it from the array. - foreach ( $variations as $i => $variation ) { - if ( $variation['name'] === $name ) { - unset( $variations[ $i ] ); - break; - } - } - // Reindex array after removing one variation. - $navigation_block_type->variations = array_values( $variations ); -} diff --git a/lib/compat/wordpress-6.5/rest-api.php b/lib/compat/wordpress-6.5/rest-api.php deleted file mode 100644 index d18756844cc91..0000000000000 --- a/lib/compat/wordpress-6.5/rest-api.php +++ /dev/null @@ -1,136 +0,0 @@ -get( 'Name' ); - return empty( $theme_name ) ? $template_object['theme'] : $theme_name; - case 'plugin': - $plugins = get_plugins(); - $plugin = $plugins[ plugin_basename( sanitize_text_field( $template_object['theme'] . '.php' ) ) ]; - return empty( $plugin['Name'] ) ? $template_object['theme'] : $plugin['Name']; - case 'site': - return get_bloginfo( 'name' ); - case 'user': - $author = get_user_by( 'id', $template_object['author'] ); - if ( ! $author ) { - return __( 'Unknown author', 'gutenberg' ); - } - return $author->get( 'display_name' ); - } -} - -/** - * Registers additional fields for wp_template and wp_template_part rest api. - * - * @access private - * @internal - */ -function _gutenberg_register_wp_templates_additional_fields() { - register_rest_field( - array( 'wp_template', 'wp_template_part' ), - 'author_text', - array( - 'get_callback' => '_gutenberg_get_wp_templates_author_text_field', - 'update_callback' => null, - 'schema' => array( - 'type' => 'string', - 'description' => __( 'Human readable text for the author.', 'gutenberg' ), - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - ) - ); - - register_rest_field( - array( 'wp_template', 'wp_template_part' ), - 'original_source', - array( - 'get_callback' => '_gutenberg_get_wp_templates_original_source_field', - 'update_callback' => null, - 'schema' => array( - 'description' => __( 'Where the template originally comes from e.g. \'theme\'', 'gutenberg' ), - 'type' => 'string', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - 'enum' => array( - 'theme', - 'plugin', - 'site', - 'user', - ), - ), - ) - ); -} - -add_action( 'rest_api_init', '_gutenberg_register_wp_templates_additional_fields' ); diff --git a/lib/compat/wordpress-6.5/script-loader.php b/lib/compat/wordpress-6.5/script-loader.php deleted file mode 100644 index a77134d9182f9..0000000000000 --- a/lib/compat/wordpress-6.5/script-loader.php +++ /dev/null @@ -1,207 +0,0 @@ -query( 'wp-date', 'registered' ) ) { - global $wp_locale; - // Calculate the timezone abbr (EDT, PST) if possible. - $timezone_string = get_option( 'timezone_string', 'UTC' ); - $timezone_abbr = ''; - - if ( ! empty( $timezone_string ) ) { - $timezone_date = new DateTime( 'now', new DateTimeZone( $timezone_string ) ); - $timezone_abbr = $timezone_date->format( 'T' ); - } - - $gmt_offset = get_option( 'gmt_offset', 0 ); - - $scripts->registered['wp-date']->extra['after'] = array( - false, - sprintf( - 'wp.date.setSettings( %s );', - wp_json_encode( - array( - 'l10n' => array( - 'locale' => get_user_locale(), - 'months' => array_values( $wp_locale->month ), - 'monthsShort' => array_values( $wp_locale->month_abbrev ), - 'weekdays' => array_values( $wp_locale->weekday ), - 'weekdaysShort' => array_values( $wp_locale->weekday_abbrev ), - 'meridiem' => (object) $wp_locale->meridiem, - 'relative' => array( - /* translators: %s: Duration. */ - 'future' => __( '%s from now', 'gutenberg' ), - /* translators: %s: Duration. */ - 'past' => __( '%s ago', 'gutenberg' ), - /* translators: One second from or to a particular datetime, e.g., "a second ago" or "a second from now". */ - 's' => __( 'a second', 'gutenberg' ), - /* translators: %d: Duration in seconds from or to a particular datetime, e.g., "4 seconds ago" or "4 seconds from now". */ - 'ss' => __( '%d seconds', 'gutenberg' ), - /* translators: One minute from or to a particular datetime, e.g., "a minute ago" or "a minute from now". */ - 'm' => __( 'a minute', 'gutenberg' ), - /* translators: %d: Duration in minutes from or to a particular datetime, e.g., "4 minutes ago" or "4 minutes from now". */ - 'mm' => __( '%d minutes', 'gutenberg' ), - /* translators: One hour from or to a particular datetime, e.g., "an hour ago" or "an hour from now". */ - 'h' => __( 'an hour', 'gutenberg' ), - /* translators: %d: Duration in hours from or to a particular datetime, e.g., "4 hours ago" or "4 hours from now". */ - 'hh' => __( '%d hours', 'gutenberg' ), - /* translators: One day from or to a particular datetime, e.g., "a day ago" or "a day from now". */ - 'd' => __( 'a day', 'gutenberg' ), - /* translators: %d: Duration in days from or to a particular datetime, e.g., "4 days ago" or "4 days from now". */ - 'dd' => __( '%d days', 'gutenberg' ), - /* translators: One month from or to a particular datetime, e.g., "a month ago" or "a month from now". */ - 'M' => __( 'a month', 'gutenberg' ), - /* translators: %d: Duration in months from or to a particular datetime, e.g., "4 months ago" or "4 months from now". */ - 'MM' => __( '%d months', 'gutenberg' ), - /* translators: One year from or to a particular datetime, e.g., "a year ago" or "a year from now". */ - 'y' => __( 'a year', 'gutenberg' ), - /* translators: %d: Duration in years from or to a particular datetime, e.g., "4 years ago" or "4 years from now". */ - 'yy' => __( '%d years', 'gutenberg' ), - ), - 'startOfWeek' => (int) get_option( 'start_of_week', 0 ), - ), - 'formats' => array( - /* translators: Time format, see https://www.php.net/manual/datetime.format.php */ - 'time' => get_option( 'time_format', __( 'g:i a', 'default' ) ), - /* translators: Date format, see https://www.php.net/manual/datetime.format.php */ - 'date' => get_option( 'date_format', __( 'F j, Y', 'default' ) ), - /* translators: Date/Time format, see https://www.php.net/manual/datetime.format.php */ - 'datetime' => __( 'F j, Y g:i a', 'default' ), - /* translators: Abbreviated date/time format, see https://www.php.net/manual/datetime.format.php */ - 'datetimeAbbreviated' => __( 'M j, Y g:i a', 'default' ), - ), - 'timezone' => array( - 'offset' => (float) $gmt_offset, - 'offsetFormatted' => str_replace( array( '.25', '.5', '.75' ), array( ':15', ':30', ':45' ), (string) $gmt_offset ), - 'string' => $timezone_string, - 'abbr' => $timezone_abbr, - ), - ) - ) - ), - ); - } -} - -add_action( 'wp_default_scripts', 'gutenberg_update_wp_date_settings' ); - -/** - * Prints inline JavaScript wrapped in ` - * - * In an HTML document this would print "…" to the console, - * but in an XHTML document it would print "…" to the console. - * - * - * - * In an HTML document this would print "An image is in HTML", - * but it's an invalid XHTML document because it interprets the `` - * as an empty tag missing its closing `/`. - * - * @see https://www.w3.org/TR/xhtml1/#h-4.8 - */ - if ( - ! $is_html5 && - ( - ! isset( $attributes['type'] ) || - 'module' === $attributes['type'] || - str_contains( $attributes['type'], 'javascript' ) || - str_contains( $attributes['type'], 'ecmascript' ) || - str_contains( $attributes['type'], 'jscript' ) || - str_contains( $attributes['type'], 'livescript' ) - ) - ) { - /* - * If the string `]]>` exists within the JavaScript it would break - * out of any wrapping CDATA section added here, so to start, it's - * necessary to escape that sequence which requires splitting the - * content into two CDATA sections wherever it's found. - * - * Note: it's only necessary to escape the closing `]]>` because - * an additional `', ']]]]>', $data ); - - // Wrap the entire escaped script inside a CDATA section. - $data = sprintf( "/* */", $data ); - } - - $data = "\n" . trim( $data, "\n\r " ) . "\n"; - - /** - * Filters attributes to be added to a script tag. - * - * @since 5.7.0 - * - * @param array $attributes Key-value pairs representing `\n", wp_sanitize_script_attributes( $attributes ), $data ); -} diff --git a/lib/compat/wordpress-6.5/scripts-modules.php b/lib/compat/wordpress-6.5/scripts-modules.php deleted file mode 100644 index 110ef858eb8ce..0000000000000 --- a/lib/compat/wordpress-6.5/scripts-modules.php +++ /dev/null @@ -1,224 +0,0 @@ -add_hooks(); - - /** - * Add module fields from block metadata to WP_Block_Type settings. - * - * This filter allows us to register modules from block metadata and attach additional fields to - * WP_Block_Type instances. - * - * @param array $settings Array of determined settings for registering a block type. - * @param array $metadata Metadata provided for registering a block type. - */ - function gutenberg_filter_block_type_metadata_settings_register_modules( $settings, $metadata = null ) { - $module_fields = array( - 'viewScriptModule' => 'view_script_module_ids', - ); - foreach ( $module_fields as $metadata_field_name => $settings_field_name ) { - if ( ! empty( $settings[ $metadata_field_name ] ) ) { - $metadata[ $metadata_field_name ] = $settings[ $metadata_field_name ]; - } - if ( ! empty( $metadata[ $metadata_field_name ] ) ) { - $modules = $metadata[ $metadata_field_name ]; - $processed_modules = array(); - if ( is_array( $modules ) ) { - for ( $index = 0; $index < count( $modules ); $index++ ) { - $processed_modules[] = gutenberg_register_block_module_id( - $metadata, - $metadata_field_name, - $index - ); - } - } else { - $processed_modules[] = gutenberg_register_block_module_id( - $metadata, - $metadata_field_name - ); - } - $settings[ $settings_field_name ] = $processed_modules; - } - } - - return $settings; - } - - add_filter( 'block_type_metadata_settings', 'gutenberg_filter_block_type_metadata_settings_register_modules', 10, 2 ); - - /** - * Enqueue modules associated with the block. - * - * @param string $block_content The block content. - * @param array $parsed_block The full block, including name and attributes. - * @param WP_Block $block_instance The block instance. - */ - function gutenberg_filter_render_block_enqueue_view_script_modules( $block_content, $parsed_block, $block_instance ) { - $block_type = $block_instance->block_type; - - if ( ! empty( $block_type->view_script_module_ids ) ) { - foreach ( $block_type->view_script_module_ids as $module_id ) { - wp_enqueue_script_module( $module_id ); - } - } - - return $block_content; - } - - add_filter( 'render_block', 'gutenberg_filter_render_block_enqueue_view_script_modules', 10, 3 ); - - /** - * Registers a REST field for block types to provide view script module IDs. - * - * Adds the `view_script_module_ids` and `view_module_ids` (deprecated) field to block type objects in the REST API, which - * lists the script module IDs for any script modules associated with the - * block's viewScriptModule key. - */ - function gutenberg_register_view_script_module_ids_rest_field() { - register_rest_field( - 'block-type', - 'view_script_module_ids', - array( - 'get_callback' => function ( $item ) { - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $item['name'] ); - if ( isset( $block_type->view_script_module_ids ) ) { - return $block_type->view_script_module_ids; - } - return array(); - }, - ) - ); - } - - add_action( 'rest_api_init', 'gutenberg_register_view_script_module_ids_rest_field' ); -} - -if ( ! function_exists( 'wp_register_script_module' ) ) { - /** - * Registers the script module if no script module with that script module - * identifier has already been registered. - * - * @since 6.5.0 - * - * @param string $id The identifier of the script module. Should be unique. It will be used in the - * final import map. - * @param string $src Optional. Full URL of the script module, or path of the script module relative - * to the WordPress root directory. If it is provided and the script module has - * not been registered yet, it will be registered. - * @param array $deps { - * Optional. List of dependencies. - * - * @type string|array $0... { - * An array of script module identifiers of the dependencies of this script - * module. The dependencies can be strings or arrays. If they are arrays, - * they need an `id` key with the script module identifier, and can contain - * an `import` key with either `static` or `dynamic`. By default, - * dependencies that don't contain an `import` key are considered static. - * - * @type string $id The script module identifier. - * @type string $import Optional. Import type. May be either `static` or - * `dynamic`. Defaults to `static`. - * } - * } - * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false. - * It is added to the URL as a query string for cache busting purposes. If $version - * is set to false, the version number is the currently installed WordPress version. - * If $version is set to null, no version is added. - */ - function wp_register_script_module( string $id, string $src, array $deps = array(), $version = false ) { - wp_script_modules()->register( $id, $src, $deps, $version ); - } -} - -if ( ! function_exists( 'wp_enqueue_script_module' ) ) { - /** - * Marks the script module to be enqueued in the page. - * - * If a src is provided and the script module has not been registered yet, it - * will be registered. - * - * @since 6.5.0 - * - * @param string $id The identifier of the script module. Should be unique. It will be used in the - * final import map. - * @param string $src Optional. Full URL of the script module, or path of the script module relative - * to the WordPress root directory. If it is provided and the script module has - * not been registered yet, it will be registered. - * @param array $deps { - * Optional. List of dependencies. - * - * @type string|array $0... { - * An array of script module identifiers of the dependencies of this script - * module. The dependencies can be strings or arrays. If they are arrays, - * they need an `id` key with the script module identifier, and can contain - * an `import` key with either `static` or `dynamic`. By default, - * dependencies that don't contain an `import` key are considered static. - * - * @type string $id The script module identifier. - * @type string $import Optional. Import type. May be either `static` or - * `dynamic`. Defaults to `static`. - * } - * } - * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false. - * It is added to the URL as a query string for cache busting purposes. If $version - * is set to false, the version number is the currently installed WordPress version. - * If $version is set to null, no version is added. - */ - function wp_enqueue_script_module( string $id, string $src = '', array $deps = array(), $version = false ) { - wp_script_modules()->enqueue( $id, $src, $deps, $version ); - } -} - -if ( ! function_exists( 'wp_dequeue_script_module' ) ) { - /** - * Unmarks the script module so it is no longer enqueued in the page. - * - * @since 6.5.0 - * - * @param string $id The identifier of the script module. - */ - function wp_dequeue_script_module( string $id ) { - wp_script_modules()->dequeue( $id ); - } -} - -if ( ! function_exists( 'wp_deregister_script_module' ) ) { - /** - * Deregisters the script module. - * - * @since 6.5.0 - * - * @param string $id The identifier of the script module. - */ - function wp_deregister_script_module( string $id ) { - wp_script_modules()->deregister( $id ); - } -} diff --git a/lib/compat/wordpress-6.6/class-gutenberg-rest-global-styles-revisions-controller-6-6.php b/lib/compat/wordpress-6.6/class-gutenberg-rest-global-styles-revisions-controller-6-6.php index f725366c33cfb..3e5d4cdd68454 100644 --- a/lib/compat/wordpress-6.6/class-gutenberg-rest-global-styles-revisions-controller-6-6.php +++ b/lib/compat/wordpress-6.6/class-gutenberg-rest-global-styles-revisions-controller-6-6.php @@ -15,7 +15,7 @@ * * @see WP_REST_Controller */ -class Gutenberg_REST_Global_Styles_Revisions_Controller_6_6 extends Gutenberg_REST_Global_Styles_Revisions_Controller_6_5 { +class Gutenberg_REST_Global_Styles_Revisions_Controller_6_6 extends WP_REST_Global_Styles_Revisions_Controller { /** * Prepares the revision for the REST response. * diff --git a/lib/compat/wordpress-6.6/class-gutenberg-rest-templates-controller-6-6.php b/lib/compat/wordpress-6.6/class-gutenberg-rest-templates-controller-6-6.php index e670afacea5b3..034187ca9a70a 100644 --- a/lib/compat/wordpress-6.6/class-gutenberg-rest-templates-controller-6-6.php +++ b/lib/compat/wordpress-6.6/class-gutenberg-rest-templates-controller-6-6.php @@ -12,7 +12,7 @@ * `edit_theme_options` capability. In order to allow other roles to also view the templates, * we need to override the permissions check for the REST API endpoints. */ -class Gutenberg_REST_Templates_Controller_6_6 extends Gutenberg_REST_Templates_Controller_6_4 { +class Gutenberg_REST_Templates_Controller_6_6 extends WP_REST_Templates_Controller { /** * Checks if a given request has access to read templates. diff --git a/lib/compat/wordpress-6.6/html-api/class-gutenberg-html-processor-6-6.php b/lib/compat/wordpress-6.6/html-api/class-gutenberg-html-processor-6-6.php index 634aaab01707c..77801535ff368 100644 --- a/lib/compat/wordpress-6.6/html-api/class-gutenberg-html-processor-6-6.php +++ b/lib/compat/wordpress-6.6/html-api/class-gutenberg-html-processor-6-6.php @@ -293,8 +293,8 @@ public static function create_fragment( $html, $context = '', $encoding = $processor->state->insertion_mode = Gutenberg_HTML_Processor_State_6_6::INSERTION_MODE_IN_BODY; // @todo Create "fake" bookmarks for non-existent but implied nodes. - $processor->bookmarks['root-node'] = new Gutenberg_HTML_Span_6_5( 0, 0 ); - $processor->bookmarks['context-node'] = new Gutenberg_HTML_Span_6_5( 0, 0 ); + $processor->bookmarks['root-node'] = new WP_HTML_Span( 0, 0 ); + $processor->bookmarks['context-node'] = new WP_HTML_Span( 0, 0 ); $processor->state->stack_of_open_elements->push( new WP_HTML_Token( diff --git a/lib/compat/wordpress-6.6/html-api/class-gutenberg-html-tag-processor-6-6.php b/lib/compat/wordpress-6.6/html-api/class-gutenberg-html-tag-processor-6-6.php index eb873e7a06340..ecbc6d0d7b6af 100644 --- a/lib/compat/wordpress-6.6/html-api/class-gutenberg-html-tag-processor-6-6.php +++ b/lib/compat/wordpress-6.6/html-api/class-gutenberg-html-tag-processor-6-6.php @@ -1227,7 +1227,7 @@ public function set_bookmark( $name ) { return false; } - $this->bookmarks[ $name ] = new Gutenberg_HTML_Span_6_5( $this->token_starts_at, $this->token_length ); + $this->bookmarks[ $name ] = new WP_HTML_Span( $this->token_starts_at, $this->token_length ); return true; } @@ -2035,7 +2035,7 @@ private function parse_next_attribute() { * an array when encountering duplicates avoids needless allocations in the * normative case of parsing tags with no duplicate attributes. */ - $duplicate_span = new Gutenberg_HTML_Span_6_5( $attribute_start, $attribute_end - $attribute_start ); + $duplicate_span = new WP_HTML_Span( $attribute_start, $attribute_end - $attribute_start ); if ( null === $this->duplicate_attributes ) { $this->duplicate_attributes = array( $comparable_name => array( $duplicate_span ) ); } elseif ( ! array_key_exists( $comparable_name, $this->duplicate_attributes ) ) { @@ -3007,7 +3007,7 @@ public function set_attribute( $name, $value ) { * Result:
*/ $existing_attribute = $this->attributes[ $comparable_name ]; - $this->lexical_updates[ $comparable_name ] = new Gutenberg_HTML_Text_Replacement_6_5( + $this->lexical_updates[ $comparable_name ] = new WP_HTML_Text_Replacement( $existing_attribute->start, $existing_attribute->length, $updated_attribute @@ -3025,7 +3025,7 @@ public function set_attribute( $name, $value ) { * * Result:
*/ - $this->lexical_updates[ $comparable_name ] = new Gutenberg_HTML_Text_Replacement_6_5( + $this->lexical_updates[ $comparable_name ] = new WP_HTML_Text_Replacement( $this->tag_name_starts_at + $this->tag_name_length, 0, ' ' . $updated_attribute @@ -3103,7 +3103,7 @@ public function remove_attribute( $name ) { * * Result:
*/ - $this->lexical_updates[ $name ] = new Gutenberg_HTML_Text_Replacement_6_5( + $this->lexical_updates[ $name ] = new WP_HTML_Text_Replacement( $this->attributes[ $name ]->start, $this->attributes[ $name ]->length, '' @@ -3112,7 +3112,7 @@ public function remove_attribute( $name ) { // Removes any duplicated attributes if they were also present. if ( null !== $this->duplicate_attributes && array_key_exists( $name, $this->duplicate_attributes ) ) { foreach ( $this->duplicate_attributes[ $name ] as $attribute_token ) { - $this->lexical_updates[] = new Gutenberg_HTML_Text_Replacement_6_5( + $this->lexical_updates[] = new WP_HTML_Text_Replacement( $attribute_token->start, $attribute_token->length, '' diff --git a/lib/compat/wordpress-6.6/rest-api.php b/lib/compat/wordpress-6.6/rest-api.php index 54eaaf28de82d..fee9c71b86c07 100644 --- a/lib/compat/wordpress-6.6/rest-api.php +++ b/lib/compat/wordpress-6.6/rest-api.php @@ -33,7 +33,7 @@ function wp_api_template_access_controller( $args, $post_type ) { /** * Adds the post classes to the REST API response. * - * @param array $post The response object data. + * @param array $post The response object data. * * @return array */ @@ -169,7 +169,7 @@ function gutenberg_block_editor_preload_paths_6_6( $paths, $context ) { if ( 'core/edit-post' === $context->name ) { $paths[] = '/wp/v2/global-styles/themes/' . get_stylesheet(); $paths[] = '/wp/v2/themes?context=edit&status=active'; - $paths[] = '/wp/v2/global-styles/' . WP_Theme_JSON_Resolver::get_user_global_styles_post_id() . '?context=edit'; + $paths[] = '/wp/v2/global-styles/' . WP_Theme_JSON_Resolver_Gutenberg::get_user_global_styles_post_id() . '?context=edit'; } return $paths; } diff --git a/lib/compat/wordpress-6.7/block-bindings.php b/lib/compat/wordpress-6.7/block-bindings.php index 4c82dc6683f37..398b53b340673 100644 --- a/lib/compat/wordpress-6.7/block-bindings.php +++ b/lib/compat/wordpress-6.7/block-bindings.php @@ -1,6 +1,6 @@ namespace = '__experimental'; - $this->rest_base = 'customizer-nonces'; - } - - /** - * Registers the necessary REST API routes. - * - * @access public - */ - public function register_routes() { - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/get-save-nonce', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_save_nonce' ), - 'permission_callback' => array( $this, 'permissions_check' ), - 'args' => $this->get_collection_params(), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - } - - /** - * Checks if a given request has access to read menu items if they have access to edit them. - * - * @return true|WP_Error True if the request has read access, WP_Error object otherwise. - */ - public function permissions_check() { - $post_type = get_post_type_object( 'nav_menu_item' ); - if ( ! current_user_can( $post_type->cap->edit_posts ) ) { - return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit posts in this post type.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); - } - return true; - } - - /** - * Returns the nonce required to request the customizer API endpoint. - * - * @access public - */ - public function get_save_nonce() { - require_once ABSPATH . 'wp-includes/class-wp-customize-manager.php'; - $wp_customize = new WP_Customize_Manager(); - $nonce = wp_create_nonce( 'save-customize_' . $wp_customize->get_stylesheet() ); - return array( - 'success' => true, - 'nonce' => $nonce, - 'stylesheet' => $wp_customize->get_stylesheet(), - ); - } - } -} diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index 09ac4b6990d91..c34984baa0a61 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -34,18 +34,37 @@ function gutenberg_enable_experiments() { if ( $gutenberg_experiments && array_key_exists( 'gutenberg-quick-edit-dataviews', $gutenberg_experiments ) ) { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalQuickEditDataViews = true', 'before' ); } + if ( $gutenberg_experiments && array_key_exists( 'gutenberg-block-bindings-ui', $gutenberg_experiments ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalBlockBindingsUI = true', 'before' ); + } } add_action( 'admin_init', 'gutenberg_enable_experiments' ); /** * Sets a global JS variable used to trigger the availability of form & input blocks. + * + * @deprecated 19.0.0 Use gutenberg_enable_block_experiments(). */ function gutenberg_enable_form_input_blocks() { + _deprecated_function( __FUNCTION__, 'Gutenberg 19.0.0', 'gutenberg_enable_block_experiments' ); +} + +/** + * Sets global JS variables used to enable various block experiments. + */ +function gutenberg_enable_block_experiments() { $gutenberg_experiments = get_option( 'gutenberg-experiments' ); + + // Experimental form blocks. if ( $gutenberg_experiments && array_key_exists( 'gutenberg-form-blocks', $gutenberg_experiments ) ) { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableFormBlocks = true', 'before' ); } + + // General experimental blocks that are not in the default block library. + if ( $gutenberg_experiments && array_key_exists( 'gutenberg-block-experiments', $gutenberg_experiments ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableBlockExperiments = true', 'before' ); + } } -add_action( 'admin_init', 'gutenberg_enable_form_input_blocks' ); +add_action( 'admin_init', 'gutenberg_enable_block_experiments' ); diff --git a/lib/experimental/posts/load.php b/lib/experimental/posts/load.php index b702a374bd16d..7321392b11a25 100644 --- a/lib/experimental/posts/load.php +++ b/lib/experimental/posts/load.php @@ -29,7 +29,7 @@ function gutenberg_posts_dashboard() { ); $editor_settings = get_block_editor_settings( $custom_settings, $block_editor_context ); - $active_global_styles_id = WP_Theme_JSON_Resolver::get_user_global_styles_post_id(); + $active_global_styles_id = WP_Theme_JSON_Resolver_Gutenberg::get_user_global_styles_post_id(); $active_theme = get_stylesheet(); $preload_paths = array( diff --git a/lib/experimental/rest-api.php b/lib/experimental/rest-api.php index 77f7d091d2655..6bb2947f88914 100644 --- a/lib/experimental/rest-api.php +++ b/lib/experimental/rest-api.php @@ -10,15 +10,6 @@ die( 'Silence is golden.' ); } -/** - * Registers the customizer nonces REST API routes. - */ -function gutenberg_register_rest_customizer_nonces() { - $customizer_nonces = new WP_Rest_Customizer_Nonces(); - $customizer_nonces->register_routes(); -} -add_action( 'rest_api_init', 'gutenberg_register_rest_customizer_nonces' ); - /** * Registers the Block editor settings REST API routes. */ diff --git a/lib/experimental/script-modules.php b/lib/experimental/script-modules.php index 709ab322f63a3..0093c2e974568 100644 --- a/lib/experimental/script-modules.php +++ b/lib/experimental/script-modules.php @@ -165,11 +165,12 @@ function gutenberg_register_view_module_ids_rest_field() { * Registers the module if no module with that module identifier has already * been registered. * + * @deprecated 17.6.0 gutenberg_register_module is deprecated. Please use wp_register_script_module instead. + * * @param string $module_identifier The identifier of the module. Should be unique. It will be used in the final import map. * @param string $src Full URL of the module, or path of the script relative to the WordPress root directory. * @param array $dependencies Optional. An array of module identifiers of the dependencies of this module. The dependencies can be strings or arrays. If they are arrays, they need an `id` key with the module identifier, and can contain an `import` key with either `static` or `dynamic`. By default, dependencies that don't contain an import are considered static. * @param string|false|null $version Optional. String specifying module version number. Defaults to false. It is added to the URL as a query string for cache busting purposes. If $version is set to false, the version number is the currently installed WordPress version. If $version is set to null, no version is added. - * @deprecated 17.6.0 gutenberg_register_module is deprecated. Please use wp_register_script_module instead. */ function gutenberg_register_module( $module_identifier, $src = '', $dependencies = array(), $version = false ) { _deprecated_function( __FUNCTION__, 'Gutenberg 17.6.0', 'wp_register_script_module' ); @@ -179,8 +180,9 @@ function gutenberg_register_module( $module_identifier, $src = '', $dependencies /** * Marks the module to be enqueued in the page. * - * @param string $module_identifier The identifier of the module. * @deprecated 17.6.0 gutenberg_enqueue_module is deprecated. Please use wp_enqueue_script_module instead. + * + * @param string $module_identifier The identifier of the module. */ function gutenberg_enqueue_module( $module_identifier ) { _deprecated_function( __FUNCTION__, 'Gutenberg 17.6.0', 'wp_enqueue_script_module' ); @@ -190,8 +192,9 @@ function gutenberg_enqueue_module( $module_identifier ) { /** * Unmarks the module so it is not longer enqueued in the page. * - * @param string $module_identifier The identifier of the module. * @deprecated 17.6.0 gutenberg_dequeue_module is deprecated. Please use wp_dequeue_script_module instead. + * + * @param string $module_identifier The identifier of the module. */ function gutenberg_dequeue_module( $module_identifier ) { _deprecated_function( __FUNCTION__, 'Gutenberg 17.6.0', 'wp_dequeue_script_module' ); diff --git a/lib/experiments-page.php b/lib/experiments-page.php index 7cc4198c14ef9..b27f6fc2726a2 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -79,6 +79,18 @@ function gutenberg_initialize_experiments_settings() { ) ); + add_settings_field( + 'gutenberg-block-experiments', + __( 'Experimental blocks', 'gutenberg' ), + 'gutenberg_display_experiment_field', + 'gutenberg-experiments', + 'gutenberg_experiments_section', + array( + 'label' => __( 'Enable experimental blocks.

(Warning: these blocks may have significant changes during development that cause validation errors and display issues.)

', 'gutenberg' ), + 'id' => 'gutenberg-block-experiments', + ) + ); + add_settings_field( 'gutenberg-form-blocks', __( 'Form and input blocks ', 'gutenberg' ), @@ -163,6 +175,18 @@ function gutenberg_initialize_experiments_settings() { ) ); + add_settings_field( + 'gutenberg-block-bindings-ui', + __( 'UI to create block bindings', 'gutenberg' ), + 'gutenberg_display_experiment_field', + 'gutenberg-experiments', + 'gutenberg_experiments_section', + array( + 'label' => __( 'Add UI to create and update block bindings in block inspector controls.', 'gutenberg' ), + 'id' => 'gutenberg-block-bindings-ui', + ) + ); + register_setting( 'gutenberg-experiments', 'gutenberg-experiments' diff --git a/lib/load.php b/lib/load.php index ef1b5cfe50b6a..5a299f3b69696 100644 --- a/lib/load.php +++ b/lib/load.php @@ -35,17 +35,6 @@ function gutenberg_is_experiment_enabled( $name ) { require_once __DIR__ . '/experimental/class-wp-rest-block-editor-settings-controller.php'; } - // WordPress 6.4 compat. - require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-templates-controller-6-4.php'; - require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php'; - require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-block-patterns-controller.php'; - require_once __DIR__ . '/compat/wordpress-6.4/rest-api.php'; - require_once __DIR__ . '/compat/wordpress-6.4/theme-previews.php'; - - // WordPress 6.5 compat. - require_once __DIR__ . '/compat/wordpress-6.5/class-gutenberg-rest-global-styles-revisions-controller-6-5.php'; - require_once __DIR__ . '/compat/wordpress-6.5/rest-api.php'; - // WordPress 6.6 compat. require __DIR__ . '/compat/wordpress-6.6/class-gutenberg-rest-global-styles-revisions-controller-6-6.php'; require __DIR__ . '/compat/wordpress-6.6/class-gutenberg-rest-templates-controller-6-6.php'; @@ -56,12 +45,7 @@ function gutenberg_is_experiment_enabled( $name ) { require_once __DIR__ . '/class-wp-rest-edit-site-export-controller-gutenberg.php'; require_once __DIR__ . '/rest-api.php'; - // Experimental. - if ( ! class_exists( 'WP_Rest_Customizer_Nonces' ) ) { - require_once __DIR__ . '/experimental/class-wp-rest-customizer-nonces.php'; - } require_once __DIR__ . '/experimental/rest-api.php'; - require_once __DIR__ . '/experimental/kses-allowed-html.php'; } @@ -74,26 +58,12 @@ function gutenberg_is_experiment_enabled( $name ) { // Gutenberg plugin compat. require __DIR__ . '/compat/plugin/edit-site-routes-backwards-compat.php'; -require __DIR__ . '/compat/plugin/footnotes.php'; +require __DIR__ . '/compat/plugin/fonts.php'; // The Token Map was created during 6.6 in order to support the HTML API. It must be loaded before it. require __DIR__ . '/compat/wordpress-6.6/class-gutenberg-token-map-6-6.php'; require __DIR__ . '/compat/wordpress-6.7/class-gutenberg-token-map-6-7.php'; -/* - * There are upstream updates to the Tag Processor that may not appear if Gutenberg is running - * a version of WordPress newer than 6.4 and older than the latest `trunk`. This file should - * always be loaded so that Gutenberg code can run the newest version of the Tag Processor. - */ -require __DIR__ . '/compat/wordpress-6.4/html-api/class-gutenberg-html-tag-processor-6-4.php'; -require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-attribute-token-6-5.php'; -require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-span-6-5.php'; -require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-text-replacement-6-5.php'; -require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-open-elements-6-5.php'; -require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-processor-state-6-5.php'; -require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-tag-processor-6-5.php'; -require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-processor-6-5.php'; - require __DIR__ . '/compat/wordpress-6.6/html-api/gutenberg-html5-named-character-references-6-6.php'; require __DIR__ . '/compat/wordpress-6.6/html-api/class-gutenberg-html-decoder-6-6.php'; require __DIR__ . '/compat/wordpress-6.6/html-api/class-gutenberg-html-tag-processor-6-6.php'; @@ -116,46 +86,6 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.7/html-api/class-gutenberg-html-processor-state-6-7.php'; require __DIR__ . '/compat/wordpress-6.7/html-api/class-gutenberg-html-processor-6-7.php'; -/* - * The HTML Processor appeared after WordPress 6.3. If Gutenberg is running on a version of - * WordPress before it was introduced, these verbatim Core files will be missing. - */ -if ( ! class_exists( 'WP_HTML_Processor' ) ) { - require __DIR__ . '/compat/wordpress-6.4/html-api/class-wp-html-active-formatting-elements.php'; - require __DIR__ . '/compat/wordpress-6.4/html-api/class-wp-html-open-elements.php'; - require __DIR__ . '/compat/wordpress-6.4/html-api/class-wp-html-processor-state.php'; - require __DIR__ . '/compat/wordpress-6.4/html-api/class-wp-html-token.php'; - require __DIR__ . '/compat/wordpress-6.4/html-api/class-wp-html-unsupported-exception.php'; - require __DIR__ . '/compat/wordpress-6.4/html-api/class-wp-html-processor.php'; -} - -// WordPress 6.4 compat. -require __DIR__ . '/compat/wordpress-6.4/blocks.php'; -require __DIR__ . '/compat/wordpress-6.4/block-hooks.php'; -require __DIR__ . '/compat/wordpress-6.4/script-loader.php'; -require __DIR__ . '/compat/wordpress-6.4/kses.php'; - -// WordPress 6.5 compat. -require __DIR__ . '/compat/wordpress-6.5/compat.php'; -require __DIR__ . '/compat/wordpress-6.5/blocks.php'; -require __DIR__ . '/compat/wordpress-6.5/block-patterns.php'; -require __DIR__ . '/compat/wordpress-6.5/kses.php'; -require __DIR__ . '/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php'; -require __DIR__ . '/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api-directives-processor.php'; -require __DIR__ . '/compat/wordpress-6.5/interactivity-api/interactivity-api.php'; -require __DIR__ . '/compat/wordpress-6.5/class-wp-script-modules.php'; -require __DIR__ . '/compat/wordpress-6.5/scripts-modules.php'; -require __DIR__ . '/compat/wordpress-6.5/navigation-block-variations.php'; -if ( ! class_exists( 'WP_Block_Bindings_Source' ) ) { - require __DIR__ . '/compat/wordpress-6.5/block-bindings/class-wp-block-bindings-source.php'; -} -if ( ! class_exists( 'WP_Block_Bindings_Registry' ) ) { - require __DIR__ . '/compat/wordpress-6.5/block-bindings/class-wp-block-bindings-registry.php'; -} -require __DIR__ . '/compat/wordpress-6.5/block-bindings/block-bindings.php'; -require __DIR__ . '/compat/wordpress-6.5/block-bindings/post-meta.php'; -require __DIR__ . '/compat/wordpress-6.5/script-loader.php'; - // WordPress 6.6 compat. require __DIR__ . '/compat/wordpress-6.6/admin-bar.php'; require __DIR__ . '/compat/wordpress-6.6/blocks.php'; @@ -185,31 +115,6 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/disable-tinymce.php'; } -// Fonts API / Font Face. -remove_action( 'plugins_loaded', '_wp_theme_json_webfonts_handler' ); // Turns off WordPress 6.0's stopgap handler. - -// Loads the Font Library. -require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-font-collection.php'; -require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-font-library.php'; -require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-font-utils.php'; -require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-rest-font-families-controller.php'; -require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php'; -require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-rest-font-collections-controller.php'; -require __DIR__ . '/compat/wordpress-6.5/fonts/fonts.php'; - -// Load the Font Face and Font Face Resolver, if not already loaded by WordPress Core. -if ( ! class_exists( 'WP_Font_Face' ) ) { - require __DIR__ . '/compat/wordpress-6.4/fonts/font-face/class-wp-font-face.php'; - require __DIR__ . '/compat/wordpress-6.4/fonts/font-face/class-wp-font-face-resolver.php'; -} - -/* - * As _gutenberg_get_iframed_editor_assets_6_4() overrides Core's _wp_get_iframed_editor_assets(), - * load this file to ensure wp_print_font_faces() is invoked to load the styles into the - * iframed editor. - */ -require __DIR__ . '/compat/wordpress-6.4/fonts/fonts.php'; - // Load the BC Layer to avoid fatal errors of extenders using the Fonts API. // @core-merge: do not merge the BC layer files into WordPress Core. require __DIR__ . '/experimental/font-face/bc-layer/class-wp-fonts-provider.php'; diff --git a/package-lock.json b/package-lock.json index 6680025d768be..a28512799804a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "18.9.0-rc.1", + "version": "19.0.0-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "18.9.0-rc.1", + "version": "19.0.0-rc.1", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -86,7 +86,7 @@ "@actions/core": "1.9.1", "@actions/github": "5.0.0", "@apidevtools/json-schema-ref-parser": "11.6.4", - "@ariakit/test": "^0.3.7", + "@ariakit/test": "^0.4.0", "@babel/core": "7.24.3", "@babel/plugin-proposal-export-namespace-from": "7.18.9", "@babel/plugin-syntax-jsx": "7.24.1", @@ -1527,55 +1527,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ariakit/core": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.3.10.tgz", - "integrity": "sha512-AcN+GSoVXuUOzKx5d3xPL3YsEHevh4PIO6QIt/mg/nRX1XQ6cvxQEiAjO/BJQm+/MVl7/VbuGBoTFjr0tPU6NQ==" - }, - "node_modules/@ariakit/react": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.3.12.tgz", - "integrity": "sha512-HxKMZZhWSkwwS/Sh9OdWyuNKQ2tjDAIQIy2KVI7IRa8ZQ6ze/4g3YLUHbfCxO7oDupXHfXaeZ4hWx8lP7l1U/g==", - "dependencies": { - "@ariakit/react-core": "0.3.12" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ariakit" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - } - }, - "node_modules/@ariakit/react-core": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.3.12.tgz", - "integrity": "sha512-w6P1A7TYb1fKUe9QbwaoTOWofl13g7TEuXdV4JyefJCQL1e9HQdEw9UL67I8aXRo8/cFHH94/z0N37t8hw5Ogg==", - "dependencies": { - "@ariakit/core": "0.3.10", - "@floating-ui/dom": "^1.0.0", - "use-sync-external-store": "^1.2.0" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - } - }, "node_modules/@ariakit/test": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.3.7.tgz", - "integrity": "sha512-rOa9pJA0ZfPPSI4SkDX41CsBcvxs6BmxgzFEElZWZo/uBBqtnr8ZL4oe5HySeZKEAHRH86XDqfxFISkhV76m5g==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.4.0.tgz", + "integrity": "sha512-AcrppK61/AbsMDyDS3AxY3WXI6fcL+WedNpJm44Qx603dVYkS/potk0PrD1MfdC6aRt+2bRRj0n9dLN5lVMtbg==", "dev": true, + "license": "MIT", "dependencies": { - "@ariakit/core": "0.3.10", - "@testing-library/dom": "^8.0.0 || ^9.0.0" + "@ariakit/core": "0.4.7", + "@testing-library/dom": "^8.0.0 || ^9.0.0 || ^10.0.0" }, "peerDependencies": { - "@testing-library/react": "^12.0.0 || ^13.0.0 || ^14.0.0", - "react": "^17.0.0 || ^18.0.0" + "@playwright/test": "^1.27.0", + "@testing-library/react": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { + "@playwright/test": { + "optional": true + }, "@testing-library/react": { "optional": true }, @@ -1584,6 +1554,12 @@ } } }, + "node_modules/@ariakit/test/node_modules/@ariakit/core": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.7.tgz", + "integrity": "sha512-GUy/3ZY4kW1KdYHtMZRrRlj5FYbZTAyHlimxHI7Zs0xsC+kAzIf8lopnf67Y9IYLgEMr37KosIV7kwpkJpNs5Q==", + "dev": true + }, "node_modules/@aw-web-design/x-default-browser": { "version": "1.4.126", "resolved": "https://registry.npmjs.org/@aw-web-design/x-default-browser/-/x-default-browser-1.4.126.tgz", @@ -52645,7 +52621,7 @@ "version": "28.4.0", "license": "GPL-2.0-or-later", "dependencies": { - "@ariakit/react": "^0.3.12", + "@ariakit/react": "^0.4.7", "@babel/runtime": "^7.16.0", "@emotion/cache": "^11.7.1", "@emotion/css": "^11.7.1", @@ -52701,6 +52677,41 @@ "react-dom": "^18.0.0" } }, + "packages/components/node_modules/@ariakit/core": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.7.tgz", + "integrity": "sha512-GUy/3ZY4kW1KdYHtMZRrRlj5FYbZTAyHlimxHI7Zs0xsC+kAzIf8lopnf67Y9IYLgEMr37KosIV7kwpkJpNs5Q==" + }, + "packages/components/node_modules/@ariakit/react": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.7.tgz", + "integrity": "sha512-uUruuCo1M0Nj2oq1nTwDfUlVTLuoI9xeHP75EkuXX46lg5hzE5vVWbSMO1D6MCy7UwrUx2Ts4IqxdKr97suTwQ==", + "dependencies": { + "@ariakit/react-core": "0.4.7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ariakit" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "packages/components/node_modules/@ariakit/react-core": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.7.tgz", + "integrity": "sha512-OogUyQ20cxkRNRuqLI05JbmpR4Lr5HwhUIqnb/sipzt6bkg/3wCXEnUAjfxg3nPjLTMjJ8+ODWmPC9JMJTW/yg==", + "dependencies": { + "@ariakit/core": "0.4.7", + "@floating-ui/dom": "^1.0.0", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "packages/components/node_modules/@floating-ui/react-dom": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.1.tgz", @@ -52969,7 +52980,7 @@ "version": "4.0.0", "license": "GPL-2.0-or-later", "dependencies": { - "@ariakit/react": "^0.3.12", + "@ariakit/react": "^0.4.7", "@babel/runtime": "^7.16.0", "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", @@ -52979,6 +52990,7 @@ "@wordpress/icons": "file:../icons", "@wordpress/primitives": "file:../primitives", "@wordpress/private-apis": "file:../private-apis", + "@wordpress/warning": "file:../warning", "clsx": "^2.1.1", "remove-accents": "^0.5.0" }, @@ -52991,38 +53003,38 @@ } }, "packages/dataviews/node_modules/@ariakit/core": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.1.tgz", - "integrity": "sha512-Rdhw0/K0x+50gFvzuMW9wp+WJxpkrgiMgegRTOZSU92bv1K+6XfQWnlieIkLt2FC7pZGrDpGlS4C7ztEVF+JRg==" + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.7.tgz", + "integrity": "sha512-GUy/3ZY4kW1KdYHtMZRrRlj5FYbZTAyHlimxHI7Zs0xsC+kAzIf8lopnf67Y9IYLgEMr37KosIV7kwpkJpNs5Q==" }, "packages/dataviews/node_modules/@ariakit/react": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.1.tgz", - "integrity": "sha512-hKfCYjc3MFW20kn2dcvejB5zbYt/uU33Teq82c414/utf5sEoeRF+bxjNku8x1baJby9/SDP6zj2IgWPuedFNA==", + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.7.tgz", + "integrity": "sha512-uUruuCo1M0Nj2oq1nTwDfUlVTLuoI9xeHP75EkuXX46lg5hzE5vVWbSMO1D6MCy7UwrUx2Ts4IqxdKr97suTwQ==", "dependencies": { - "@ariakit/react-core": "0.4.1" + "@ariakit/react-core": "0.4.7" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/ariakit" }, "peerDependencies": { - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "packages/dataviews/node_modules/@ariakit/react-core": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.1.tgz", - "integrity": "sha512-cwDczl9XWBloXNg0CuHmJtBfEe7qF265JE0Pwlcp8wMSY9PsJeb0mKBlTygUPKn/FsKpKGaYSI7DlDntbcZciw==", + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.7.tgz", + "integrity": "sha512-OogUyQ20cxkRNRuqLI05JbmpR4Lr5HwhUIqnb/sipzt6bkg/3wCXEnUAjfxg3nPjLTMjJ8+ODWmPC9JMJTW/yg==", "dependencies": { - "@ariakit/core": "0.4.1", + "@ariakit/core": "0.4.7", "@floating-ui/dom": "^1.0.0", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "packages/date": { @@ -53728,7 +53740,6 @@ "license": "GPL-2.0-or-later", "dependencies": { "@preact/signals": "^1.2.2", - "deepsignal": "^1.4.0", "preact": "^10.19.3" }, "engines": { @@ -53763,31 +53774,6 @@ "preact": "10.x" } }, - "packages/interactivity/node_modules/deepsignal": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/deepsignal/-/deepsignal-1.4.0.tgz", - "integrity": "sha512-x0XUMT48s+xQRLc2fPFfxnYLCJ46vffw47OQ5NcHFzacOjfW5eA0NrEmI0bhQHL6MgUHkBVT4TIiWTVwzTEwpg==", - "peerDependencies": { - "@preact/signals": "^1.1.4", - "@preact/signals-core": "^1.5.1", - "@preact/signals-react": "^1.3.8 || ^2.0.0", - "preact": "^10.16.0" - }, - "peerDependenciesMeta": { - "@preact/signals": { - "optional": true - }, - "@preact/signals-core": { - "optional": true - }, - "@preact/signals-react": { - "optional": true - }, - "preact": { - "optional": true - } - } - }, "packages/interactivity/node_modules/preact": { "version": "10.19.3", "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.3.tgz", @@ -54181,6 +54167,9 @@ "engines": { "node": ">=18.12.0", "npm": ">=8.19.2" + }, + "peerDependencies": { + "react": "^18.0.0" } }, "packages/priority-queue": { @@ -56146,37 +56135,22 @@ } } }, - "@ariakit/core": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.3.10.tgz", - "integrity": "sha512-AcN+GSoVXuUOzKx5d3xPL3YsEHevh4PIO6QIt/mg/nRX1XQ6cvxQEiAjO/BJQm+/MVl7/VbuGBoTFjr0tPU6NQ==" - }, - "@ariakit/react": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.3.12.tgz", - "integrity": "sha512-HxKMZZhWSkwwS/Sh9OdWyuNKQ2tjDAIQIy2KVI7IRa8ZQ6ze/4g3YLUHbfCxO7oDupXHfXaeZ4hWx8lP7l1U/g==", - "requires": { - "@ariakit/react-core": "0.3.12" - } - }, - "@ariakit/react-core": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.3.12.tgz", - "integrity": "sha512-w6P1A7TYb1fKUe9QbwaoTOWofl13g7TEuXdV4JyefJCQL1e9HQdEw9UL67I8aXRo8/cFHH94/z0N37t8hw5Ogg==", - "requires": { - "@ariakit/core": "0.3.10", - "@floating-ui/dom": "^1.0.0", - "use-sync-external-store": "^1.2.0" - } - }, "@ariakit/test": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.3.7.tgz", - "integrity": "sha512-rOa9pJA0ZfPPSI4SkDX41CsBcvxs6BmxgzFEElZWZo/uBBqtnr8ZL4oe5HySeZKEAHRH86XDqfxFISkhV76m5g==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.4.0.tgz", + "integrity": "sha512-AcrppK61/AbsMDyDS3AxY3WXI6fcL+WedNpJm44Qx603dVYkS/potk0PrD1MfdC6aRt+2bRRj0n9dLN5lVMtbg==", "dev": true, "requires": { - "@ariakit/core": "0.3.10", - "@testing-library/dom": "^8.0.0 || ^9.0.0" + "@ariakit/core": "0.4.7", + "@testing-library/dom": "^8.0.0 || ^9.0.0 || ^10.0.0" + }, + "dependencies": { + "@ariakit/core": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.7.tgz", + "integrity": "sha512-GUy/3ZY4kW1KdYHtMZRrRlj5FYbZTAyHlimxHI7Zs0xsC+kAzIf8lopnf67Y9IYLgEMr37KosIV7kwpkJpNs5Q==", + "dev": true + } } }, "@aw-web-design/x-default-browser": { @@ -67524,7 +67498,7 @@ "@wordpress/components": { "version": "file:packages/components", "requires": { - "@ariakit/react": "^0.3.12", + "@ariakit/react": "^0.4.7", "@babel/runtime": "^7.16.0", "@emotion/cache": "^11.7.1", "@emotion/css": "^11.7.1", @@ -67572,6 +67546,29 @@ "uuid": "^9.0.1" }, "dependencies": { + "@ariakit/core": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.7.tgz", + "integrity": "sha512-GUy/3ZY4kW1KdYHtMZRrRlj5FYbZTAyHlimxHI7Zs0xsC+kAzIf8lopnf67Y9IYLgEMr37KosIV7kwpkJpNs5Q==" + }, + "@ariakit/react": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.7.tgz", + "integrity": "sha512-uUruuCo1M0Nj2oq1nTwDfUlVTLuoI9xeHP75EkuXX46lg5hzE5vVWbSMO1D6MCy7UwrUx2Ts4IqxdKr97suTwQ==", + "requires": { + "@ariakit/react-core": "0.4.7" + } + }, + "@ariakit/react-core": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.7.tgz", + "integrity": "sha512-OogUyQ20cxkRNRuqLI05JbmpR4Lr5HwhUIqnb/sipzt6bkg/3wCXEnUAjfxg3nPjLTMjJ8+ODWmPC9JMJTW/yg==", + "requires": { + "@ariakit/core": "0.4.7", + "@floating-ui/dom": "^1.0.0", + "use-sync-external-store": "^1.2.0" + } + }, "@floating-ui/react-dom": { "version": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.1.tgz", "integrity": "sha512-rZtAmSht4Lry6gdhAJDrCp/6rKN7++JnL1/Anbr/DdeyYXQPxvg/ivrbYvJulbRf4vL8b212suwMM2lxbv+RQA==", @@ -67757,7 +67754,7 @@ "@wordpress/dataviews": { "version": "file:packages/dataviews", "requires": { - "@ariakit/react": "^0.3.12", + "@ariakit/react": "^0.4.7", "@babel/runtime": "^7.16.0", "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", @@ -67767,28 +67764,30 @@ "@wordpress/icons": "file:../icons", "@wordpress/primitives": "file:../primitives", "@wordpress/private-apis": "file:../private-apis", + "@wordpress/warning": "file:../warning", "clsx": "^2.1.1", "remove-accents": "^0.5.0" }, "dependencies": { "@ariakit/core": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.1.tgz", - "integrity": "sha512-Rdhw0/K0x+50gFvzuMW9wp+WJxpkrgiMgegRTOZSU92bv1K+6XfQWnlieIkLt2FC7pZGrDpGlS4C7ztEVF+JRg==" + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.7.tgz", + "integrity": "sha512-GUy/3ZY4kW1KdYHtMZRrRlj5FYbZTAyHlimxHI7Zs0xsC+kAzIf8lopnf67Y9IYLgEMr37KosIV7kwpkJpNs5Q==" }, "@ariakit/react": { - "version": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.1.tgz", - "integrity": "sha512-hKfCYjc3MFW20kn2dcvejB5zbYt/uU33Teq82c414/utf5sEoeRF+bxjNku8x1baJby9/SDP6zj2IgWPuedFNA==", + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.7.tgz", + "integrity": "sha512-uUruuCo1M0Nj2oq1nTwDfUlVTLuoI9xeHP75EkuXX46lg5hzE5vVWbSMO1D6MCy7UwrUx2Ts4IqxdKr97suTwQ==", "requires": { - "@ariakit/react-core": "0.4.1" + "@ariakit/react-core": "0.4.7" } }, "@ariakit/react-core": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.1.tgz", - "integrity": "sha512-cwDczl9XWBloXNg0CuHmJtBfEe7qF265JE0Pwlcp8wMSY9PsJeb0mKBlTygUPKn/FsKpKGaYSI7DlDntbcZciw==", + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.7.tgz", + "integrity": "sha512-OogUyQ20cxkRNRuqLI05JbmpR4Lr5HwhUIqnb/sipzt6bkg/3wCXEnUAjfxg3nPjLTMjJ8+ODWmPC9JMJTW/yg==", "requires": { - "@ariakit/core": "0.4.1", + "@ariakit/core": "0.4.7", "@floating-ui/dom": "^1.0.0", "use-sync-external-store": "^1.2.0" } @@ -68270,7 +68269,6 @@ "version": "file:packages/interactivity", "requires": { "@preact/signals": "^1.2.2", - "deepsignal": "^1.4.0", "preact": "^10.19.3" }, "dependencies": { @@ -68282,11 +68280,6 @@ "@preact/signals-core": "^1.4.0" } }, - "deepsignal": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/deepsignal/-/deepsignal-1.4.0.tgz", - "integrity": "sha512-x0XUMT48s+xQRLc2fPFfxnYLCJ46vffw47OQ5NcHFzacOjfW5eA0NrEmI0bhQHL6MgUHkBVT4TIiWTVwzTEwpg==" - }, "preact": { "version": "10.19.3", "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.3.tgz", diff --git a/package.json b/package.json index 412ff34b91d8c..fac57093a852c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "18.9.0-rc.1", + "version": "19.0.0-rc.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", @@ -98,7 +98,7 @@ "@actions/core": "1.9.1", "@actions/github": "5.0.0", "@apidevtools/json-schema-ref-parser": "11.6.4", - "@ariakit/test": "^0.3.7", + "@ariakit/test": "^0.4.0", "@babel/core": "7.24.3", "@babel/plugin-proposal-export-namespace-from": "7.18.9", "@babel/plugin-syntax-jsx": "7.24.1", diff --git a/packages/base-styles/_animations.scss b/packages/base-styles/_animations.scss index b5e6655e660cb..bdfd7595da8e4 100644 --- a/packages/base-styles/_animations.scss +++ b/packages/base-styles/_animations.scss @@ -3,3 +3,8 @@ animation-fill-mode: forwards; @include reduce-motion("animation"); } + +@mixin editor-canvas-resize-animation() { + transition: all 0.5s cubic-bezier(0.65, 0, 0.45, 1); + @include reduce-motion("transition"); +} diff --git a/packages/base-styles/_mixins.scss b/packages/base-styles/_mixins.scss index 506d12af84b2f..69735d75aac71 100644 --- a/packages/base-styles/_mixins.scss +++ b/packages/base-styles/_mixins.scss @@ -162,22 +162,6 @@ } } -@mixin placeholder-style() { - border-radius: $radius-block-ui; - - &::before { - content: ""; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - pointer-events: none; - background: currentColor; - opacity: 0.1; - } -} - /** * Allows users to opt-out of animations via OS-level preferences. */ diff --git a/packages/base-styles/_variables.scss b/packages/base-styles/_variables.scss index b10eeeb167dad..97eb513cf38ae 100644 --- a/packages/base-styles/_variables.scss +++ b/packages/base-styles/_variables.scss @@ -22,7 +22,6 @@ $text-editor-font-size: 15px; $editor-line-height: 1.8; $mobile-text-min-font-size: 16px; // Any font size below 16px will cause Mobile Safari to "zoom in". - /** * Grid System. * https://make.wordpress.org/design/2019/10/31/proposal-a-consistent-spacing-system-for-wordpress/ @@ -40,6 +39,17 @@ $grid-unit-60: 6 * $grid-unit; // 48px $grid-unit-70: 7 * $grid-unit; // 56px $grid-unit-80: 8 * $grid-unit; // 64px +/** + * Radius scale. + */ + +$radius-x-small: 1px; // Applied to elements like buttons nested within primitives like inputs. +$radius-small: 2px; // Applied to most primitives. +$radius-medium: 4px; // Applied to containers with smaller padding. +$radius-large: 8px; // Applied to containers with larger padding. +$radius-full: 9999px; // For lozenges. +$radius-round: 50%; // For circles and ovals. + /** * Dimensions. */ @@ -80,7 +90,7 @@ $sidebar-width: 280px; $content-width: 840px; $wide-content-width: 1100px; $widget-area-width: 700px; - +$secondary-sidebar-width: 350px; /** * Block & Editor UI. @@ -91,13 +101,12 @@ $border-width: 1px; $border-width-focus-fallback: 2px; // This exists as a fallback, and is ideally overridden by var(--wp-admin-border-width-focus) unless in some SASS math cases. $border-width-tab: 1.5px; $helptext-font-size: 12px; -$radius-round: 50%; -$radius-block-ui: 2px; $radio-input-size: 16px; $radio-input-size-sm: 24px; // Width & height for small viewports. // Deprecated, please avoid using these. $block-padding: 14px; // Used to define space between block footprint and surrouding borders. +$radius-block-ui: $radius-small; /** diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index b8a9088cfd72c..4d5f22e02fa7d 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -208,6 +208,9 @@ $z-layers: ( // Ensure checkbox + actions don't overlap table header ".dataviews-view-table thead": 1, + // Ensure selection checkbox stays above the preview field. + ".dataviews-view-grid__card .dataviews-selection-checkbox": 1, + // Ensure quick actions toolbar appear above pagination ".dataviews-bulk-actions-toolbar": 2, ); diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md index 11cc15c15f0b0..bc7d1b88bc9b0 100644 --- a/packages/block-editor/CHANGELOG.md +++ b/packages/block-editor/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +### Breaking Changes + +- `URLInput`: Remove deprecated `__nextHasNoMarginBottom` prop and promote to default behavior ([#64282](https://github.com/WordPress/gutenberg/pull/64282)). +- `LineHeightControl`: Remove deprecated `__nextHasNoMarginBottom` prop and promote to default behavior ([#64281](https://github.com/WordPress/gutenberg/pull/64281)). + +### Enhancements + +- `FontFamilyControl`: Add `__nextHasNoMarginBottom` prop for opting into the new margin-free styles ([#64280](https://github.com/WordPress/gutenberg/pull/64280)). + ## 13.4.0 (2024-07-24) ## 13.3.0 (2024-07-10) diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 00951fb94c4a7..776b217ba54f6 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -547,7 +547,7 @@ _Returns_ > **Deprecated** -This function was accidentially exposed for mobile/native usage. +This function was accidentally exposed for mobile/native usage. _Returns_ @@ -1029,6 +1029,16 @@ _Returns_ - `any[]`: Returns the values defined for the settings. +### useStyleOverride + +Override a block editor settings style. Leave the ID blank to create a new style. + +_Parameters_ + +- _override_ `Object`: Override object. +- _override.id_ `?string`: Id of the style override, leave blank to create a new style. +- _override.css_ `string`: CSS to apply. + ### useZoomOut A hook used to set the editor mode to zoomed out mode, invoking the hook sets the mode. diff --git a/packages/block-editor/src/autocompleters/style.scss b/packages/block-editor/src/autocompleters/style.scss index 5380ddcfbe48b..43acc4efc16a5 100644 --- a/packages/block-editor/src/autocompleters/style.scss +++ b/packages/block-editor/src/autocompleters/style.scss @@ -4,6 +4,10 @@ .block-editor-block-icon { margin-right: $grid-unit-10; } + + &[aria-selected="true"] .block-editor-block-icon { + color: inherit !important; + } } .block-editor-autocompleters__link { diff --git a/packages/block-editor/src/components/block-breadcrumb/index.js b/packages/block-editor/src/components/block-breadcrumb/index.js index b3f2d3dee1201..8bd790c5c8fb2 100644 --- a/packages/block-editor/src/components/block-breadcrumb/index.js +++ b/packages/block-editor/src/components/block-breadcrumb/index.js @@ -5,6 +5,7 @@ import { Button } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { chevronRightSmall, Icon } from '@wordpress/icons'; +import { useRef } from '@wordpress/element'; /** * Internal dependencies @@ -12,7 +13,7 @@ import { chevronRightSmall, Icon } from '@wordpress/icons'; import BlockTitle from '../block-title'; import { store as blockEditorStore } from '../../store'; import { unlock } from '../../lock-unlock'; -import { __unstableUseBlockRef as useBlockRef } from '../block-list/use-block-props/use-block-refs'; +import { useBlockElementRef } from '../block-list/use-block-props/use-block-refs'; import getEditorRegion from '../../utils/get-editor-region'; /** @@ -41,7 +42,8 @@ function BlockBreadcrumb( { rootLabelText } ) { // We don't care about this specific ref, but this is a way // to get a ref within the editor canvas so we can focus it later. - const blockRef = useBlockRef( clientId ); + const blockRef = useRef(); + useBlockElementRef( clientId, blockRef ); /* * Disable reason: The `list` ARIA role is redundant but diff --git a/packages/block-editor/src/components/block-canvas/style.scss b/packages/block-editor/src/components/block-canvas/style.scss index 9e924cb79bace..1395b5c0a437d 100644 --- a/packages/block-editor/src/components/block-canvas/style.scss +++ b/packages/block-editor/src/components/block-canvas/style.scss @@ -4,4 +4,5 @@ iframe[name="editor-canvas"] { height: 100%; display: block; background-color: transparent; + @include editor-canvas-resize-animation; } diff --git a/packages/block-editor/src/components/block-draggable/index.js b/packages/block-editor/src/components/block-draggable/index.js index 0ba2b857bc693..e1afc1f251384 100644 --- a/packages/block-editor/src/components/block-draggable/index.js +++ b/packages/block-editor/src/components/block-draggable/index.js @@ -13,7 +13,7 @@ import { throttle } from '@wordpress/compose'; import BlockDraggableChip from './draggable-chip'; import useScrollWhenDragging from './use-scroll-when-dragging'; import { store as blockEditorStore } from '../../store'; -import { __unstableUseBlockRef as useBlockRef } from '../block-list/use-block-props/use-block-refs'; +import { useBlockElement } from '../block-list/use-block-props/use-block-refs'; import { isDropTargetValid } from '../use-block-drop-zone'; const BlockDraggable = ( { @@ -82,8 +82,8 @@ const BlockDraggable = ( { }, [] ); // Find the root of the editor iframe. - const blockRef = useBlockRef( clientIds[ 0 ] ); - const editorRoot = blockRef.current?.closest( 'body' ); + const blockEl = useBlockElement( clientIds[ 0 ] ); + const editorRoot = blockEl?.closest( 'body' ); /* * Add a dragover event listener to the editor root to track the blocks being dragged over. diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-block-refs.js b/packages/block-editor/src/components/block-list/use-block-props/use-block-refs.js index 056ade045d165..16fd3ff1ca81d 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/use-block-refs.js +++ b/packages/block-editor/src/components/block-list/use-block-props/use-block-refs.js @@ -1,13 +1,7 @@ /** * WordPress dependencies */ -import { - useContext, - useMemo, - useRef, - useState, - useLayoutEffect, -} from '@wordpress/element'; +import { useContext, useState, useLayoutEffect } from '@wordpress/element'; import { useRefEffect } from '@wordpress/compose'; /** @@ -16,7 +10,7 @@ import { useRefEffect } from '@wordpress/compose'; import { BlockRefs } from '../../provider/block-refs-provider'; /** @typedef {import('@wordpress/element').RefCallback} RefCallback */ -/** @typedef {import('@wordpress/element').RefObject} RefObject */ +/** @typedef {import('@wordpress/element').Ref} Ref */ /** * Provides a ref to the BlockRefs context. @@ -36,31 +30,33 @@ export function useBlockRefProvider( clientId ) { ); } +function assignRef( ref, value ) { + if ( typeof ref === 'function' ) { + ref( value ); + } else if ( ref ) { + ref.current = value; + } +} + /** - * Gets a ref pointing to the current block element. Continues to return the same - * stable ref object even if the `clientId` argument changes. This hook is not - * reactive, i.e., it won't trigger a rerender of the calling component if the - * ref value changes. For reactive use cases there is the `useBlockElement` hook. - * - * @param {string} clientId The client ID to get a ref for. + * Tracks the DOM element for the block identified by `clientId` and assigns it to the `ref` + * whenever it changes. * - * @return {RefObject} A ref containing the element. + * @param {string} clientId The client ID to track. + * @param {Ref} ref The ref object/callback to assign to. */ -function useBlockRef( clientId ) { +export function useBlockElementRef( clientId, ref ) { const { refsMap } = useContext( BlockRefs ); - const latestClientId = useRef(); - latestClientId.current = clientId; - - // Always return an object, even if no ref exists for a given client ID, so - // that `current` works at a later point. - return useMemo( - () => ( { - get current() { - return refsMap.get( latestClientId.current ) ?? null; - }, - } ), - [ refsMap ] - ); + useLayoutEffect( () => { + assignRef( ref, refsMap.get( clientId ) ); + const unsubscribe = refsMap.subscribe( clientId, () => + assignRef( ref, refsMap.get( clientId ) ) + ); + return () => { + unsubscribe(); + assignRef( ref, null ); + }; + }, [ refsMap, clientId, ref ] ); } /** @@ -71,20 +67,8 @@ function useBlockRef( clientId ) { * * @return {Element|null} The block's wrapper element. */ -function useBlockElement( clientId ) { - const { refsMap } = useContext( BlockRefs ); +export function useBlockElement( clientId ) { const [ blockElement, setBlockElement ] = useState( null ); - // Delay setting the resulting `blockElement` until an effect. If the block element - // changes (i.e., the block is unmounted and re-mounted), this allows enough time - // for the ref callbacks to clean up the old element and set the new one. - useLayoutEffect( () => { - setBlockElement( refsMap.get( clientId ) ); - return refsMap.subscribe( clientId, () => - setBlockElement( refsMap.get( clientId ) ) - ); - }, [ refsMap, clientId ] ); + useBlockElementRef( clientId, setBlockElement ); return blockElement; } - -export { useBlockRef as __unstableUseBlockRef }; -export { useBlockElement as __unstableUseBlockElement }; diff --git a/packages/block-editor/src/components/block-mover/index.js b/packages/block-editor/src/components/block-mover/index.js index aaaf6c64b5595..7aa273605bbe6 100644 --- a/packages/block-editor/src/components/block-mover/index.js +++ b/packages/block-editor/src/components/block-mover/index.js @@ -69,7 +69,11 @@ function BlockMover( { [ clientIds ] ); - if ( ! canMove || ( isFirst && isLast && ! rootClientId ) ) { + if ( + ! canMove || + ( isFirst && isLast && ! rootClientId ) || + ( hideDragHandle && isManualGrid ) + ) { return null; } @@ -85,7 +89,6 @@ function BlockMover( {
); diff --git a/packages/block-editor/src/components/block-tools/use-block-toolbar-popover-props.js b/packages/block-editor/src/components/block-tools/use-block-toolbar-popover-props.js index f99323dd5c80a..0ca0f6e5a43dd 100644 --- a/packages/block-editor/src/components/block-tools/use-block-toolbar-popover-props.js +++ b/packages/block-editor/src/components/block-tools/use-block-toolbar-popover-props.js @@ -15,7 +15,7 @@ import { * Internal dependencies */ import { store as blockEditorStore } from '../../store'; -import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs'; +import { useBlockElement } from '../block-list/use-block-props/use-block-refs'; import { hasStickyOrFixedPositionValue } from '../../hooks/position'; const COMMON_PROPS = { diff --git a/packages/block-editor/src/components/block-tools/zoom-out-mode-inserters.js b/packages/block-editor/src/components/block-tools/zoom-out-mode-inserters.js index 5d273a4f3f6d5..bb044f9479c02 100644 --- a/packages/block-editor/src/components/block-tools/zoom-out-mode-inserters.js +++ b/packages/block-editor/src/components/block-tools/zoom-out-mode-inserters.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useSelect } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; import { useEffect, useState } from '@wordpress/element'; /** @@ -16,8 +16,9 @@ function ZoomOutModeInserters() { const [ isReady, setIsReady ] = useState( false ); const { hasSelection, + blockInsertionPoint, blockOrder, - insertionPoint, + blockInsertionPointVisible, setInserterIsOpened, sectionRootClientId, selectedBlockClientId, @@ -25,23 +26,19 @@ function ZoomOutModeInserters() { } = useSelect( ( select ) => { const { getSettings, + getBlockInsertionPoint, getBlockOrder, getSelectionStart, getSelectedBlockClientId, getHoveredBlockClientId, + isBlockInsertionPointVisible, } = select( blockEditorStore ); const { sectionRootClientId: root } = unlock( getSettings() ); - // To do: move ZoomOutModeInserters to core/editor. - // Or we perhaps we should move the insertion point state to the - // block-editor store. I'm not sure what it was ever moved to the editor - // store, because all the inserter components all live in the - // block-editor package. - // eslint-disable-next-line @wordpress/data-no-store-string-literals - const editor = select( 'core/editor' ); return { hasSelection: !! getSelectionStart().clientId, + blockInsertionPoint: getBlockInsertionPoint(), blockOrder: getBlockOrder( root ), - insertionPoint: unlock( editor ).getInsertionPoint(), + blockInsertionPointVisible: isBlockInsertionPointVisible(), sectionRootClientId: root, setInserterIsOpened: getSettings().__experimentalSetIsInserterOpened, @@ -50,6 +47,8 @@ function ZoomOutModeInserters() { }; }, [] ); + const { showInsertionPoint } = useDispatch( blockEditorStore ); + // Defer the initial rendering to avoid the jumps due to the animation. useEffect( () => { const timeout = setTimeout( () => { @@ -65,14 +64,8 @@ function ZoomOutModeInserters() { } return [ undefined, ...blockOrder ].map( ( clientId, index ) => { - const shouldRenderInserter = insertionPoint.insertionIndex !== index; - const shouldRenderInsertionPoint = - insertionPoint.insertionIndex === index; - - if ( ! shouldRenderInserter && ! shouldRenderInsertionPoint ) { - return null; - } + blockInsertionPointVisible && blockInsertionPoint.index === index; const previousClientId = clientId; const nextClientId = blockOrder[ index ]; @@ -104,7 +97,7 @@ function ZoomOutModeInserters() { className="block-editor-block-list__insertion-point-indicator" /> ) } - { shouldRenderInserter && ( + { ! shouldRenderInsertionPoint && ( { @@ -114,6 +107,9 @@ function ZoomOutModeInserters() { tab: 'patterns', category: 'all', } ); + showInsertionPoint( sectionRootClientId, index, { + operation: 'insert', + } ); } } /> ) } diff --git a/packages/block-editor/src/components/block-tools/zoom-out-toolbar.js b/packages/block-editor/src/components/block-tools/zoom-out-toolbar.js index bb5955244cd82..4dcadc798489a 100644 --- a/packages/block-editor/src/components/block-tools/zoom-out-toolbar.js +++ b/packages/block-editor/src/components/block-tools/zoom-out-toolbar.js @@ -100,7 +100,6 @@ export default function ZoomOutToolbar( { clientId, rootClientId } ) {
- - { /* Dummy ToolsPanel items, so we can control what's in the dropdown popover */ } - hasImageValue } - label={ __( 'Image' ) } - onDeselect={ resetBackground } - isShownByDefault={ defaultControls.backgroundImage } - panelId={ panelId } - className="block-editor-global-styles-background-panel__hidden-tools-panel-item" - /> ); } diff --git a/packages/block-editor/src/components/global-styles/image-settings-panel.js b/packages/block-editor/src/components/global-styles/image-settings-panel.js index f668e7e5efc24..4ebc20ab20198 100644 --- a/packages/block-editor/src/components/global-styles/image-settings-panel.js +++ b/packages/block-editor/src/components/global-styles/image-settings-panel.js @@ -67,6 +67,7 @@ export default function ImageSettingsPanel( { panelId={ panelId } > { - const themeJson = { - styles: { - background: { - backgroundImage: { - url: 'file:./assets/image.jpg', - }, - }, - }, - }; - - it( 'should replace relative paths with resolved URIs if found in themeFileURIs', () => { - const newThemeJson = setThemeFileUris( themeJson, themeFileURIs ); - expect( - newThemeJson.styles.background.backgroundImage.url === - 'https://wordpress.org/assets/image.jpg' - ).toBe( true ); - // Object reference should be the same as the function is mutating the object. - expect( newThemeJson ).toEqual( themeJson ); - } ); -} ); - describe( 'getResolvedThemeFilePath()', () => { it.each( [ [ diff --git a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js index 525a8a1d53d07..f648e1db845b8 100644 --- a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js @@ -1028,14 +1028,59 @@ describe( 'global styles renderer', () => { 'letter-spacing: 2px', ] ); } ); + it( 'should set default values for block background styles', () => { + const backgroundStyles = { + background: { + backgroundImage: { + url: 'https://wordpress.org/assets/image.jpg', + id: 123, + }, + }, + }; + expect( + getStylesDeclarations( backgroundStyles, '.wp-block-group' ) + ).toEqual( [ + "background-image: url( 'https://wordpress.org/assets/image.jpg' )", + 'background-size: cover', + ] ); + // Test with root-level styles. + expect( + getStylesDeclarations( backgroundStyles, ROOT_BLOCK_SELECTOR ) + ).toEqual( [ + "background-image: url( 'https://wordpress.org/assets/image.jpg' )", + ] ); + expect( + getStylesDeclarations( + { + background: { + ...backgroundStyles.background, + backgroundSize: 'contain', + }, + }, + '.wp-block-group' + ) + ).toEqual( [ + "background-image: url( 'https://wordpress.org/assets/image.jpg' )", + 'background-position: center', + 'background-size: contain', + ] ); + } ); } ); describe( 'processCSSNesting', () => { + it( 'should return empty string when supplied css is empty', () => { + expect( processCSSNesting( '', '.foo' ) ).toEqual( '' ); + } ); it( 'should return processed CSS without any nested selectors', () => { expect( processCSSNesting( 'color: red; margin: auto;', '.foo' ) ).toEqual( ':root :where(.foo){color: red; margin: auto;}' ); } ); + it( 'should return processed CSS when there are no root selectors', () => { + expect( + processCSSNesting( '&::before{color: red;}', '.foo' ) + ).toEqual( ':root :where(.foo)::before{color: red;}' ); + } ); it( 'should return processed CSS with nested selectors', () => { expect( processCSSNesting( @@ -1049,21 +1094,21 @@ describe( 'global styles renderer', () => { it( 'should return processed CSS with pseudo elements', () => { expect( processCSSNesting( - 'color: red; margin: auto; &::before{color: blue;} & ::before{color: green;} &.one::before{color: yellow;} & .two::before{color: purple;}', + 'color: red; margin: auto; &::before{color: blue;} & ::before{color: green;} &.one::before{color: yellow;} & .two::before{color: purple;} & > ::before{color: darkseagreen;}', '.foo' ) ).toEqual( - ':root :where(.foo){color: red; margin: auto;}:root :where(.foo::before){color: blue;}:root :where(.foo ::before){color: green;}:root :where(.foo.one::before){color: yellow;}:root :where(.foo .two::before){color: purple;}' + ':root :where(.foo){color: red; margin: auto;}:root :where(.foo)::before{color: blue;}:root :where(.foo) ::before{color: green;}:root :where(.foo.one)::before{color: yellow;}:root :where(.foo .two)::before{color: purple;}:root :where(.foo) > ::before{color: darkseagreen;}' ); } ); it( 'should return processed CSS with multiple root selectors', () => { expect( processCSSNesting( - 'color: red; margin: auto; &.one{color: blue;} & .two{color: green;} &::before{color: yellow;} & ::before{color: purple;} &.three::before{color: orange;} & .four::before{color: skyblue;}', + 'color: red; margin: auto; &.one{color: blue;} & .two{color: green;} &::before{color: yellow;} & ::before{color: purple;} &.three::before{color: orange;} & .four::before{color: skyblue;} & > ::before{color: darkseagreen;}', '.foo, .bar' ) ).toEqual( - ':root :where(.foo, .bar){color: red; margin: auto;}:root :where(.foo.one, .bar.one){color: blue;}:root :where(.foo .two, .bar .two){color: green;}:root :where(.foo::before, .bar::before){color: yellow;}:root :where(.foo ::before, .bar ::before){color: purple;}:root :where(.foo.three::before, .bar.three::before){color: orange;}:root :where(.foo .four::before, .bar .four::before){color: skyblue;}' + ':root :where(.foo, .bar){color: red; margin: auto;}:root :where(.foo.one, .bar.one){color: blue;}:root :where(.foo .two, .bar .two){color: green;}:root :where(.foo, .bar)::before{color: yellow;}:root :where(.foo, .bar) ::before{color: purple;}:root :where(.foo.three, .bar.three)::before{color: orange;}:root :where(.foo .four, .bar .four)::before{color: skyblue;}:root :where(.foo, .bar) > ::before{color: darkseagreen;}' ); } ); } ); diff --git a/packages/block-editor/src/components/global-styles/test/utils.js b/packages/block-editor/src/components/global-styles/test/utils.js index f06a09494ab9f..08ed1f18e8040 100644 --- a/packages/block-editor/src/components/global-styles/test/utils.js +++ b/packages/block-editor/src/components/global-styles/test/utils.js @@ -7,6 +7,9 @@ import { getPresetVariableFromValue, getValueFromVariable, scopeFeatureSelectors, + getResolvedThemeFilePath, + getResolvedRefValue, + getResolvedValue, } from '../utils'; describe( 'editor utils', () => { @@ -52,6 +55,41 @@ describe( 'editor utils', () => { }, }, }, + styles: { + background: { + backgroundImage: { + url: 'file:./assets/image.jpg', + }, + backgroundAttachment: 'fixed', + backgroundPosition: 'top left', + }, + blocks: { + 'core/group': { + background: { + backgroundImage: { + ref: 'styles.background.backgroundImage', + }, + }, + dimensions: { + minHeight: '100px', + }, + }, + }, + }, + _links: { + 'wp:theme-file': [ + { + name: 'file:./assets/image.jpg', + href: 'https://wordpress.org/assets/image.jpg', + target: 'styles.background.backgroundImage.url', + }, + { + name: 'file:./assets/other/image.jpg', + href: 'https://wordpress.org/assets/other/image.jpg', + target: "styles.blocks.['core/group'].background.backgroundImage.url", + }, + ], + }, isGlobalStylesUserThemeJSON: true, }; @@ -366,4 +404,86 @@ describe( 'editor utils', () => { } ); } ); } ); + + describe( 'getResolvedThemeFilePath()', () => { + it.each( [ + [ + 'file:./assets/image.jpg', + 'https://wordpress.org/assets/image.jpg', + 'Should return absolute URL if found in themeFileURIs', + ], + [ + 'file:./misc/image.jpg', + 'file:./misc/image.jpg', + 'Should return value if not found in themeFileURIs', + ], + [ + 'https://wordpress.org/assets/image.jpg', + 'https://wordpress.org/assets/image.jpg', + 'Should not match absolute URLs', + ], + ] )( + 'Given file %s and return value %s: %s', + ( file, returnedValue ) => { + expect( + getResolvedThemeFilePath( + file, + themeJson._links[ 'wp:theme-file' ] + ) === returnedValue + ).toBe( true ); + } + ); + } ); + + describe( 'getResolvedRefValue()', () => { + it.each( [ + [ 'blue', 'blue', null ], + [ 0, 0, themeJson ], + [ + { ref: 'styles.background.backgroundImage' }, + { url: 'file:./assets/image.jpg' }, + themeJson, + ], + [ + { + ref: 'styles.blocks.core/group.background.backgroundImage', + }, + undefined, + themeJson, + ], + ] )( + 'Given ruleValue %s return expected value of %s', + ( ruleValue, returnedValue, tree ) => { + expect( getResolvedRefValue( ruleValue, tree ) ).toEqual( + returnedValue + ); + } + ); + } ); + + describe( 'getResolvedValue()', () => { + it.each( [ + [ 'blue', 'blue', null ], + [ 0, 0, themeJson ], + [ + { ref: 'styles.background.backgroundImage' }, + { url: 'https://wordpress.org/assets/image.jpg' }, + themeJson, + ], + [ + { + ref: 'styles.blocks.core/group.background.backgroundImage', + }, + undefined, + themeJson, + ], + ] )( + 'Given ruleValue %s return expected value of %s', + ( ruleValue, returnedValue, tree ) => { + expect( getResolvedValue( ruleValue, tree ) ).toEqual( + returnedValue + ); + } + ); + } ); } ); diff --git a/packages/block-editor/src/components/global-styles/theme-file-uri-utils.js b/packages/block-editor/src/components/global-styles/theme-file-uri-utils.js index 1ab05a45f0d54..96b3e2e4cb68b 100644 --- a/packages/block-editor/src/components/global-styles/theme-file-uri-utils.js +++ b/packages/block-editor/src/components/global-styles/theme-file-uri-utils.js @@ -1,8 +1,3 @@ -/** - * Internal dependencies - */ -import { getValueFromObjectPath } from '../../utils/object'; - /** * Looks up a theme file URI based on a relative path. * @@ -21,57 +16,3 @@ export function getResolvedThemeFilePath( file, themeFileURIs = [] ) { return uri?.href; } - -/** - * Mutates an object by settings a value at the provided path. - * - * @param {Object} object Object to set a value in. - * @param {number|string|Array} path Path in the object to modify. - * @param {*} value New value to set. - * @return {Object} Object with the new value set. - */ -function setMutably( object, path, value ) { - path = path.split( '.' ); - const finalValueKey = path.pop(); - let prev = object; - - for ( const key of path ) { - const current = prev[ key ]; - prev = current; - } - - prev[ finalValueKey ] = value; - - return object; -} - -/** - * Resolves any relative paths if a corresponding theme file URI is available. - * Note: this function mutates the object and is specifically to be used in - * an async styles build context in useGlobalStylesOutput - * - * @param {Object} themeJson Theme.json/Global styles tree. - * @param {Array} themeFileURIs A collection of absolute theme file URIs and their corresponding file paths. - * @return {Object} Returns mutated object. - */ -export function setThemeFileUris( themeJson, themeFileURIs ) { - if ( ! themeJson?.styles || ! themeFileURIs ) { - return themeJson; - } - - themeFileURIs.forEach( ( { name, href, target } ) => { - const value = getValueFromObjectPath( themeJson, target ); - if ( value === name ) { - /* - * The object must not be updated immutably here because the - * themeJson is a reference to the global styles tree used as a dependency in the - * useGlobalStylesOutputWithConfig() hook. If we don't mutate the object, - * the hook will detect the change and re-render the component, resulting - * in a maximum depth exceeded error. - */ - themeJson = setMutably( themeJson, target, href ); - } - } ); - - return themeJson; -} diff --git a/packages/block-editor/src/components/global-styles/typography-panel.js b/packages/block-editor/src/components/global-styles/typography-panel.js index f6a389a5bc96d..9bc875cdc0a30 100644 --- a/packages/block-editor/src/components/global-styles/typography-panel.js +++ b/packages/block-editor/src/components/global-styles/typography-panel.js @@ -474,7 +474,6 @@ export default function TypographyPanel( { panelId={ panelId } > { // Don't output padding properties if padding variables are set or if we're not editing a full template. @@ -424,18 +429,7 @@ export function getStylesDeclarations( ? rule.key : kebabCase( rule.key ); - let ruleValue = rule.value; - if ( typeof ruleValue !== 'string' && ruleValue?.ref ) { - const refPath = ruleValue.ref.split( '.' ); - ruleValue = compileStyleValue( - getValueFromObjectPath( tree, refPath ) - ); - // Presence of another ref indicates a reference to another dynamic value. - // Pointing to another dynamic value is not supported. - if ( ! ruleValue || ruleValue?.ref ) { - return; - } - } + let ruleValue = getResolvedValue( rule.value, tree, null ); // Calculate fluid typography rules where available. if ( cssProperty === 'font-size' ) { @@ -1327,9 +1321,17 @@ function updateConfigWithSeparator( config ) { export function processCSSNesting( css, blockSelector ) { let processedCSS = ''; + if ( ! css || css.trim() === '' ) { + return processedCSS; + } + // Split CSS nested rules. const parts = css.split( '&' ); parts.forEach( ( part ) => { + if ( ! part || part.trim() === '' ) { + return; + } + const isRootCss = ! part.includes( '{' ); if ( isRootCss ) { // If the part doesn't contain braces, it applies to the root level. @@ -1342,11 +1344,32 @@ export function processCSSNesting( css, blockSelector ) { } const [ nestedSelector, cssValue ] = splittedPart; - const combinedSelector = nestedSelector.startsWith( ' ' ) - ? scopeSelector( blockSelector, nestedSelector ) - : appendToSelector( blockSelector, nestedSelector ); - processedCSS += `:root :where(${ combinedSelector }){${ cssValue.trim() }}`; + // Handle pseudo elements such as ::before, ::after, etc. Regex will also + // capture any leading combinator such as >, +, or ~, as well as spaces. + // This allows pseudo elements as descendants e.g. `.parent ::before`. + const matches = nestedSelector.match( /([>+~\s]*::[a-zA-Z-]+)/ ); + const pseudoPart = matches ? matches[ 1 ] : ''; + const withoutPseudoElement = matches + ? nestedSelector.replace( pseudoPart, '' ).trim() + : nestedSelector.trim(); + + let combinedSelector; + if ( withoutPseudoElement === '' ) { + // Only contained a pseudo element to use the block selector to form + // the final `:root :where()` selector. + combinedSelector = blockSelector; + } else { + // If the nested selector is a descendant of the block scope it with the + // block selector. Otherwise append it to the block selector. + combinedSelector = nestedSelector.startsWith( ' ' ) + ? scopeSelector( blockSelector, withoutPseudoElement ) + : appendToSelector( blockSelector, withoutPseudoElement ); + } + + // Build final rule, re-adding any pseudo element outside the `:where()` + // to maintain valid CSS selector. + processedCSS += `:root :where(${ combinedSelector })${ pseudoPart }{${ cssValue.trim() }}`; } } ); return processedCSS; @@ -1369,10 +1392,6 @@ export function useGlobalStylesOutputWithConfig( disableRootPadding ) { const [ blockGap ] = useGlobalSetting( 'spacing.blockGap' ); - mergedConfig = setThemeFileUris( - mergedConfig, - mergedConfig?._links?.[ 'wp:theme-file' ] - ); const hasBlockGapSupport = blockGap !== null; const hasFallbackGapSupport = ! hasBlockGapSupport; // This setting isn't useful yet: it exists as a placeholder for a future explicit fallback styles support. const disableLayoutStyles = useSelect( ( select ) => { diff --git a/packages/block-editor/src/components/global-styles/utils.js b/packages/block-editor/src/components/global-styles/utils.js index bf84e6f0b5765..8de479e39382e 100644 --- a/packages/block-editor/src/components/global-styles/utils.js +++ b/packages/block-editor/src/components/global-styles/utils.js @@ -525,3 +525,115 @@ export function getBlockStyleVariationSelector( variation, blockSelector ) { return result.join( ',' ); } + +/** + * Converts style preset values `var:` to CSS custom var values. + * TODO: Export and use the style engine util: getCSSVarFromStyleValue(). + * + * Example: + * + * compileStyleValue( 'var:preset|color|primary' ) // returns 'var(--wp--color-primary)' + * + * @param {string} uncompiledValue A block style value. + * @return {string} The compiled, or original value. + */ +export function compileStyleValue( uncompiledValue ) { + const VARIABLE_REFERENCE_PREFIX = 'var:'; + if ( + 'string' === typeof uncompiledValue && + uncompiledValue?.startsWith?.( VARIABLE_REFERENCE_PREFIX ) + ) { + const VARIABLE_PATH_SEPARATOR_TOKEN_ATTRIBUTE = '|'; + const VARIABLE_PATH_SEPARATOR_TOKEN_STYLE = '--'; + const variable = uncompiledValue + .slice( VARIABLE_REFERENCE_PREFIX.length ) + .split( VARIABLE_PATH_SEPARATOR_TOKEN_ATTRIBUTE ) + .join( VARIABLE_PATH_SEPARATOR_TOKEN_STYLE ); + return `var(--wp--${ variable })`; + } + return uncompiledValue; +} + +/** + * Looks up a theme file URI based on a relative path. + * + * @param {string} file A relative path. + * @param {Array} themeFileURIs A collection of absolute theme file URIs and their corresponding file paths. + * @return {string} A resolved theme file URI, if one is found in the themeFileURIs collection. + */ +export function getResolvedThemeFilePath( file, themeFileURIs ) { + if ( ! file || ! themeFileURIs || ! Array.isArray( themeFileURIs ) ) { + return file; + } + + const uri = themeFileURIs.find( + ( themeFileUri ) => themeFileUri?.name === file + ); + + if ( ! uri?.href ) { + return file; + } + + return uri?.href; +} + +/** + * Resolves ref values in theme JSON. + * + * @param {Object|string} ruleValue A block style value that may contain a reference to a theme.json value. + * @param {Object} tree A theme.json object. + * @return {*} The resolved value or incoming ruleValue. + */ +export function getResolvedRefValue( ruleValue, tree ) { + if ( ! ruleValue || ! tree ) { + return ruleValue; + } + + if ( typeof ruleValue !== 'string' && ruleValue?.ref ) { + const refPath = ruleValue.ref.split( '.' ); + const resolvedRuleValue = compileStyleValue( + getValueFromObjectPath( tree, refPath ) + ); + + /* + * Presence of another ref indicates a reference to another dynamic value. + * Pointing to another dynamic value is not supported. + */ + if ( resolvedRuleValue?.ref ) { + return undefined; + } + + if ( ! resolvedRuleValue ) { + return ruleValue; + } + + return resolvedRuleValue; + } + return ruleValue; +} + +/** + * Resolves ref and relative path values in theme JSON. + * + * @param {Object|string} ruleValue A block style value that may contain a reference to a theme.json value. + * @param {Object} tree A theme.json object. + * @return {*} The resolved value or incoming ruleValue. + */ +export function getResolvedValue( ruleValue, tree ) { + if ( ! ruleValue || ! tree ) { + return ruleValue; + } + + // Resolve ref values. + const resolvedValue = getResolvedRefValue( ruleValue, tree ); + + // Resolve relative paths. + if ( resolvedValue?.url ) { + resolvedValue.url = getResolvedThemeFilePath( + resolvedValue.url, + tree?._links?.[ 'wp:theme-file' ] + ); + } + + return resolvedValue; +} diff --git a/packages/block-editor/src/components/grid/grid-item-movers.js b/packages/block-editor/src/components/grid/grid-item-movers.js index adda11b7a45a6..fc9a3c5bf3995 100644 --- a/packages/block-editor/src/components/grid/grid-item-movers.js +++ b/packages/block-editor/src/components/grid/grid-item-movers.js @@ -55,28 +55,29 @@ export function GridItemMovers( { return ( - { - onChange( { - columnStart: columnStart - 1, - } ); - __unstableMarkNextChangeAsNotPersistent(); - moveBlocksToPosition( - [ blockClientId ], - gridClientId, - gridClientId, - getNumberOfBlocksBeforeCell( - columnStart - 1, - rowStart - ) - ); - } } - /> +
+ { + onChange( { + columnStart: columnStart - 1, + } ); + __unstableMarkNextChangeAsNotPersistent(); + moveBlocksToPosition( + [ blockClientId ], + gridClientId, + gridClientId, + getNumberOfBlocksBeforeCell( + columnStart - 1, + rowStart + ) + ); + } } + /> +
- = columnCount } - onClick={ () => { - onChange( { - columnStart: columnStart + 1, - } ); - __unstableMarkNextChangeAsNotPersistent(); - moveBlocksToPosition( - [ blockClientId ], - gridClientId, - gridClientId, - getNumberOfBlocksBeforeCell( - columnStart + 1, - rowStart - ) - ); - } } - /> +
+ = columnCount } + onClick={ () => { + onChange( { + columnStart: columnStart + 1, + } ); + __unstableMarkNextChangeAsNotPersistent(); + moveBlocksToPosition( + [ blockClientId ], + gridClientId, + gridClientId, + getNumberOfBlocksBeforeCell( + columnStart + 1, + rowStart + ) + ); + } } + /> +
); diff --git a/packages/block-editor/src/components/grid/grid-item-resizer.js b/packages/block-editor/src/components/grid/grid-item-resizer.js index 6f6fa655b3556..da3eb824fe92e 100644 --- a/packages/block-editor/src/components/grid/grid-item-resizer.js +++ b/packages/block-editor/src/components/grid/grid-item-resizer.js @@ -7,7 +7,7 @@ import { useState, useEffect } from '@wordpress/element'; /** * Internal dependencies */ -import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs'; +import { useBlockElement } from '../block-list/use-block-props/use-block-refs'; import BlockPopoverCover from '../block-popover/cover'; import { getComputedCSS, getGridTracks, getClosestTrack } from './utils'; @@ -98,7 +98,7 @@ function GridItemResizerInner( { { + /* + * Captures the pointer to avoid hiccups while dragging over objects + * like iframes and ensures that the event to end the drag is + * captured by the target (resize handle) whether or not it’s under + * the pointer. + */ + target.setPointerCapture( pointerId ); + } } onResizeStart={ ( event, direction ) => { /* * The container justification and alignment need to be set @@ -126,21 +135,6 @@ function GridItemResizerInner( { * so that it resizes in the right direction. */ setResizeDirection( direction ); - - /* - * The mouseup event on the resize handle doesn't trigger if the mouse - * isn't directly above the handle, so we try to detect if it happens - * outside the grid and dispatch a mouseup event on the handle. - */ - blockElement.ownerDocument.addEventListener( - 'mouseup', - () => { - event.target.dispatchEvent( - new Event( 'mouseup', { bubbles: true } ) - ); - }, - { once: true } - ); } } onResizeStop={ ( event, direction, boxElement ) => { const columnGap = parseFloat( diff --git a/packages/block-editor/src/components/grid/grid-visualizer.js b/packages/block-editor/src/components/grid/grid-visualizer.js index 5e5e1e3bfa2f7..e1d35f012b4d8 100644 --- a/packages/block-editor/src/components/grid/grid-visualizer.js +++ b/packages/block-editor/src/components/grid/grid-visualizer.js @@ -13,7 +13,7 @@ import { __experimentalUseDropZone as useDropZone } from '@wordpress/compose'; /** * Internal dependencies */ -import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs'; +import { useBlockElement } from '../block-list/use-block-props/use-block-refs'; import BlockPopoverCover from '../block-popover/cover'; import { range, GridRect, getGridInfo } from './utils'; import { store as blockEditorStore } from '../../store'; diff --git a/packages/block-editor/src/components/grid/style.scss b/packages/block-editor/src/components/grid/style.scss index 32f0cfc0c62dc..6790d683ca7d0 100644 --- a/packages/block-editor/src/components/grid/style.scss +++ b/packages/block-editor/src/components/grid/style.scss @@ -105,7 +105,6 @@ .block-editor-grid-item-mover-button { width: $block-toolbar-height * 0.5; min-width: 0 !important; // overrides default button width. - overflow: hidden; padding-left: 0; padding-right: 0; @@ -155,7 +154,7 @@ justify-content: space-around; > .block-editor-grid-item-mover-button.block-editor-grid-item-mover-button { - height: $block-toolbar-height * 0.5 - $grid-unit-05; + height: $block-toolbar-height * 0.5 - $grid-unit-05 !important; // overrides toolbar button height. width: 100%; min-width: 0 !important; // overrides default button width. @@ -173,18 +172,53 @@ } } +.editor-collapsible-block-toolbar { + .block-editor-grid-item-mover__move-vertical-button-container { + // Move up a little to prevent the toolbar shift when focus is on the vertical movers. + @include break-small() { + height: $grid-unit-50; + position: relative; + top: -5px; // Should be -4px, but that causes scrolling when focus lands on the movers, in a 60px header. + } + } +} + .show-icon-labels { - .block-editor-grid-item-mover-button.block-editor-grid-item-mover-button.is-left-button { - border-right: 1px solid $gray-700; - padding-right: 12px; - } + .block-editor-grid-item-mover__move-horizontal-button-container { + position: relative; - .block-editor-grid-item-mover-button.block-editor-grid-item-mover-button.is-right-button { - border-left: 1px solid $gray-700; - padding-left: 12px; - } + &::before { + @include break-small() { + content: ""; + height: 100%; + width: $border-width; + background: $gray-200; + position: absolute; + top: 0; + } + + @include break-medium() { + background: $gray-900; + } + } + + &.is-left { + padding-right: 6px; + &::before { + right: 0; + } + } + + &.is-right { + padding-left: 6px; + + &::before { + left: 0; + } + } + } .block-editor-grid-item-mover__move-vertical-button-container { &::before { @@ -208,5 +242,21 @@ } } + .block-editor-grid-item-mover-button { + white-space: nowrap; + } + + .editor-collapsible-block-toolbar { + .block-editor-grid-item-mover__move-horizontal-button-container::before { + height: $grid-unit-30; + background: $gray-300; + top: $grid-unit-05; + } + + .block-editor-grid-item-mover__move-vertical-button-container::before { + background: $gray-300; + width: calc(100% - #{$grid-unit-30}); + } + } } diff --git a/packages/block-editor/src/components/grid/use-grid-layout-sync.js b/packages/block-editor/src/components/grid/use-grid-layout-sync.js index ed368714d63d3..3e31530d4e526 100644 --- a/packages/block-editor/src/components/grid/use-grid-layout-sync.js +++ b/packages/block-editor/src/components/grid/use-grid-layout-sync.js @@ -10,6 +10,7 @@ import { usePrevious } from '@wordpress/compose'; */ import { store as blockEditorStore } from '../../store'; import { GridRect } from './utils'; +import { setImmutably } from '../../utils/object'; export function useGridLayoutSync( { clientId: gridClientId } ) { const { gridLayout, blockOrder, selectedBlockLayout } = useSelect( @@ -26,7 +27,8 @@ export function useGridLayoutSync( { clientId: gridClientId } ) { [ gridClientId ] ); - const { getBlockAttributes } = useSelect( blockEditorStore ); + const { getBlockAttributes, getBlockRootClientId } = + useSelect( blockEditorStore ); const { updateBlockAttributes, __unstableMarkNextChangeAsNotPersistent } = useDispatch( blockEditorStore ); @@ -37,6 +39,10 @@ export function useGridLayoutSync( { clientId: gridClientId } ) { ); const previouslySelectedBlockRect = usePrevious( selectedBlockRect ); + const previousIsManualPlacement = usePrevious( + gridLayout.isManualPlacement + ); + const previousBlockOrder = usePrevious( blockOrder ); useEffect( () => { const updates = {}; @@ -120,20 +126,65 @@ export function useGridLayoutSync( { clientId: gridClientId } ) { }, }; } + + // Unset grid layout attributes for blocks removed from the grid. + for ( const clientId of previousBlockOrder ?? [] ) { + if ( ! blockOrder.includes( clientId ) ) { + const rootClientId = getBlockRootClientId( clientId ); + + // Block was removed from the editor, so nothing to do. + if ( rootClientId === null ) { + continue; + } + + // Check if the block is being moved to another grid. + // If so, do nothing and let the new grid parent handle + // the attributes. + const rootAttributes = getBlockAttributes( rootClientId ); + if ( rootAttributes?.layout?.type === 'grid' ) { + continue; + } + + const attributes = getBlockAttributes( clientId ); + const { + columnStart, + rowStart, + columnSpan, + rowSpan, + ...layout + } = attributes.style?.layout ?? {}; + + if ( columnStart || rowStart || columnSpan || rowSpan ) { + const hasEmptyLayoutAttribute = + Object.keys( layout ).length === 0; + + updates[ clientId ] = setImmutably( + attributes, + [ 'style', 'layout' ], + hasEmptyLayoutAttribute ? undefined : layout + ); + } + } + } } else { - // When in auto mode, remove all of the columnStart and rowStart values. - for ( const clientId of blockOrder ) { - const attributes = getBlockAttributes( clientId ); - const { columnStart, rowStart, ...layout } = - attributes.style?.layout ?? {}; - // Only update attributes if columnStart or rowStart are set. - if ( columnStart || rowStart ) { - updates[ clientId ] = { - style: { - ...attributes.style, - layout, - }, - }; + // Remove all of the columnStart and rowStart values + // when switching from manual to auto mode, + if ( previousIsManualPlacement === true ) { + for ( const clientId of blockOrder ) { + const attributes = getBlockAttributes( clientId ); + const { columnStart, rowStart, ...layout } = + attributes.style?.layout ?? {}; + // Only update attributes if columnStart or rowStart are set. + if ( columnStart || rowStart ) { + const hasEmptyLayoutAttribute = + Object.keys( layout ).length === 0; + + updates[ clientId ] = setImmutably( + attributes, + [ 'style', 'layout' ], + hasEmptyLayoutAttribute ? undefined : layout + ); + } } } @@ -160,11 +211,14 @@ export function useGridLayoutSync( { clientId: gridClientId } ) { // Actual deps to sync: gridClientId, gridLayout, + previousBlockOrder, blockOrder, previouslySelectedBlockRect, + previousIsManualPlacement, // These won't change, but the linter thinks they might: __unstableMarkNextChangeAsNotPersistent, getBlockAttributes, + getBlockRootClientId, updateBlockAttributes, ] ); } diff --git a/packages/block-editor/src/components/iframe/content.scss b/packages/block-editor/src/components/iframe/content.scss index 8c03d4224745a..90b887993bf6d 100644 --- a/packages/block-editor/src/components/iframe/content.scss +++ b/packages/block-editor/src/components/iframe/content.scss @@ -22,9 +22,9 @@ } .block-editor-iframe__html { + border: 0 solid $gray-300; transform-origin: top center; - transition: transform 0.3s; - @include reduce-motion("transition"); + @include editor-canvas-resize-animation; } .block-editor-iframe__html.is-zoomed-out { diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 669e2fe25a9fb..e7af77920ea12 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -242,10 +242,8 @@ function Iframe( { const isZoomedOut = scale !== 1; useEffect( () => { - if ( ! isZoomedOut ) { - prevContainerWidth.current = containerWidth; - } - }, [ containerWidth, isZoomedOut ] ); + prevContainerWidth.current = containerWidth; + }, [ containerWidth ] ); const disabledRef = useDisabled( { isDisabled: ! readonly } ); const bodyRef = useMergeRefs( [ @@ -305,7 +303,7 @@ function Iframe( { iframeDocument.documentElement.classList.add( 'is-zoomed-out' ); - const maxWidth = 800; + const maxWidth = 750; iframeDocument.documentElement.style.setProperty( '--wp-block-editor-iframe-zoom-out-scale', scale === 'default' @@ -378,10 +376,8 @@ function Iframe( {