From 0e6cc04b34aa295c81b7dd11d573cc050bf0f723 Mon Sep 17 00:00:00 2001 From: akasunil Date: Sat, 22 Jun 2024 12:54:39 +0530 Subject: [PATCH 01/23] Move link dropdown to toolbar in gallery block --- packages/block-library/src/gallery/edit.js | 70 ++++++++++++++++++---- 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index 0b0b378889070..8be09c5fd3829 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -14,6 +14,10 @@ import { ToggleControl, RangeControl, Spinner, + DropdownMenu, + MenuGroup, + MenuItem, + ToolbarGroup, } from '@wordpress/components'; import { store as blockEditorStore, @@ -32,6 +36,12 @@ import { View } from '@wordpress/primitives'; import { createBlock } from '@wordpress/blocks'; import { createBlobURL } from '@wordpress/blob'; import { store as noticesStore } from '@wordpress/notices'; +import { + link as linkIcon, + customLink, + image as imageIcon, + linkOff, +} from '@wordpress/icons'; /** * Internal dependencies @@ -56,11 +66,20 @@ import GapStyles from './gap-styles'; const MAX_COLUMNS = 8; const linkOptions = [ - { value: LINK_DESTINATION_ATTACHMENT, label: __( 'Attachment Page' ) }, - { value: LINK_DESTINATION_MEDIA, label: __( 'Media File' ) }, { - value: LINK_DESTINATION_NONE, + icon: customLink, + label: __( 'Link images to attachment pages' ), + value: LINK_DESTINATION_ATTACHMENT, + }, + { + icon: imageIcon, + label: __( 'Link images to media files' ), + value: LINK_DESTINATION_MEDIA, + }, + { + icon: linkOff, label: _x( 'None', 'Media item link option' ), + value: LINK_DESTINATION_NONE, }, ]; const ALLOWED_MEDIA_TYPES = [ 'image' ]; @@ -550,15 +569,6 @@ function GalleryEdit( props ) { size="__unstable-large" /> ) } - + + + + { ( { onClose } ) => ( + + { linkOptions.map( ( linkItem ) => { + const isOptionSelected = + linkTo === linkItem.value; + return ( + { + setLinkTo( linkItem.value ); + onClose(); + } } + role="menuitemradio" + > + { linkItem.label } + + ); + } ) } + + ) } + + + { Platform.isWeb && ( <> { ! multiGallerySelection && ( From cac26ab885e4927428c409de0c652b8a2237c69d Mon Sep 17 00:00:00 2001 From: akasunil Date: Sun, 23 Jun 2024 12:33:44 +0530 Subject: [PATCH 02/23] Update e2e test for gallery block's setting --- packages/block-library/src/gallery/test/index.native.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/gallery/test/index.native.js b/packages/block-library/src/gallery/test/index.native.js index fd95053bea62c..60adf0a103108 100644 --- a/packages/block-library/src/gallery/test/index.native.js +++ b/packages/block-library/src/gallery/test/index.native.js @@ -9,6 +9,7 @@ import { getEditorHtml, initializeEditor, openBlockSettings, + openBlockActionsMenu, setupCoreBlocks, setupMediaPicker, setupMediaUpload, @@ -613,9 +614,10 @@ describe( 'Gallery block', () => { const { getByText } = screen; // Set "Link to" setting via Gallery block settings - await openBlockSettings( screen ); + await openBlockActionsMenu( screen ); + fireEvent.press( getByText( 'Link to' ) ); - fireEvent.press( getByText( 'Media File' ) ); + fireEvent.press( getByText( 'Link images to media files' ) ); expect( getEditorHtml() ).toMatchSnapshot(); } ); From a0c3301eb6b409077d3fc0f6d7e97db2b475acec Mon Sep 17 00:00:00 2001 From: akasunil Date: Sun, 23 Jun 2024 16:44:09 +0530 Subject: [PATCH 03/23] Fix e2e test for gallery block images link setting --- .../block-library/src/gallery/test/index.native.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/block-library/src/gallery/test/index.native.js b/packages/block-library/src/gallery/test/index.native.js index 60adf0a103108..223e107b50ef3 100644 --- a/packages/block-library/src/gallery/test/index.native.js +++ b/packages/block-library/src/gallery/test/index.native.js @@ -9,7 +9,6 @@ import { getEditorHtml, initializeEditor, openBlockSettings, - openBlockActionsMenu, setupCoreBlocks, setupMediaPicker, setupMediaUpload, @@ -611,13 +610,11 @@ describe( 'Gallery block', () => { `, numberOfItems: 2, } ); - const { getByText } = screen; - - // Set "Link to" setting via Gallery block settings - await openBlockActionsMenu( screen ); + const { getByLabelText } = screen; - fireEvent.press( getByText( 'Link to' ) ); - fireEvent.press( getByText( 'Link images to media files' ) ); + fireEvent.press( getBlock( screen, 'Gallery' ) ); + fireEvent.press( getByLabelText( 'Link To' ) ); + fireEvent.press( getByLabelText( 'Link images to media files' ) ); expect( getEditorHtml() ).toMatchSnapshot(); } ); From 7e6f2c6f86c4219ebea78f3dad471dc5c778af69 Mon Sep 17 00:00:00 2001 From: akasunil Date: Tue, 25 Jun 2024 15:42:03 +0530 Subject: [PATCH 04/23] Fix unit test --- packages/block-library/src/gallery/test/index.native.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/gallery/test/index.native.js b/packages/block-library/src/gallery/test/index.native.js index 223e107b50ef3..48e6a3b1e8cca 100644 --- a/packages/block-library/src/gallery/test/index.native.js +++ b/packages/block-library/src/gallery/test/index.native.js @@ -610,11 +610,11 @@ describe( 'Gallery block', () => { `, numberOfItems: 2, } ); - const { getByLabelText } = screen; + const { getByLabelText, getByText } = screen; fireEvent.press( getBlock( screen, 'Gallery' ) ); fireEvent.press( getByLabelText( 'Link To' ) ); - fireEvent.press( getByLabelText( 'Link images to media files' ) ); + fireEvent.press( getByText( 'Link images to media files' ) ); expect( getEditorHtml() ).toMatchSnapshot(); } ); From 0c7a78b8569b436a73f1c8e43f2150a78f68601e Mon Sep 17 00:00:00 2001 From: akasunil Date: Sat, 20 Jul 2024 14:56:39 +0530 Subject: [PATCH 05/23] Synced with trunk --- .eslintrc.js | 28 + .gitattributes | 3 + .github/ISSUE_TEMPLATE/Bug_report.yml | 18 +- .github/workflows/create-block.yml | 10 +- .github/workflows/unit-test.yml | 20 +- backport-changelog/6.6/7036.md | 3 + backport-changelog/6.6/7061.md | 3 + backport-changelog/6.7/6991.md | 3 + ...e-reference.js => gen-theme-reference.mjs} | 59 +- changelog.txt | 12 +- docs/contributors/code/coding-guidelines.md | 6 +- docs/contributors/code/deprecations.md | 2 +- .../data-basics/2-building-a-list-of-pages.md | 2 +- .../platform/custom-block-editor.md | 2 +- .../block-api/block-context.md | 2 +- docs/reference-guides/core-blocks.md | 10 +- .../data/data-core-block-editor.md | 24 + .../filters/editor-filters.md | 4 +- .../interactivity-api/iapi-faq.md | 9 + .../theme-json-reference/theme-json-living.md | 1 + gutenberg.php | 2 +- lib/block-supports/background.php | 12 +- lib/block-supports/layout.php | 12 +- lib/class-wp-theme-json-gutenberg.php | 25 +- .../class-gutenberg-token-map-6-7.php | 820 ++++ ...rg-html-active-formatting-elements-6-7.php | 187 + ...ass-gutenberg-html-attribute-token-6-7.php | 116 + .../class-gutenberg-html-decoder-6-7.php | 461 +++ ...class-gutenberg-html-open-elements-6-7.php | 598 +++ .../class-gutenberg-html-processor-6-7.php | 3402 ++++++++++++++++ ...ass-gutenberg-html-processor-state-6-7.php | 417 ++ .../class-gutenberg-html-span-6-7.php | 56 + .../class-gutenberg-html-stack-event-6-7.php | 82 + ...class-gutenberg-html-tag-processor-6-7.php | 3521 +++++++++++++++++ ...ss-gutenberg-html-text-replacement-6-7.php | 64 + .../class-gutenberg-html-token-6-7.php | 106 + ...tenberg-html-unsupported-exception-6-7.php | 115 + lib/load.php | 17 +- package-lock.json | 3098 ++++----------- package.json | 11 +- packages/base-styles/_variables.scss | 2 +- packages/base-styles/_z-index.scss | 3 +- packages/block-editor/README.md | 4 - .../src/components/alignment-control/ui.js | 4 +- .../components/block-alignment-control/ui.js | 2 +- .../block-alignment-control/ui.native.js | 2 +- .../src/components/block-context/README.md | 8 +- .../src/components/block-inspector/index.js | 12 +- .../block-list/use-block-props/index.js | 2 +- .../use-block-props/use-block-refs.js | 22 +- .../use-block-props/use-is-hovered.js | 37 +- .../src/components/block-switcher/index.js | 2 +- .../pattern-transformations-menu.js | 32 +- .../block-switcher/preview-block-popover.js | 34 +- .../src/components/block-switcher/style.scss | 25 +- .../src/components/block-toolbar/shuffle.js | 3 +- .../src/components/block-tools/style.scss | 8 + .../block-tools/use-show-block-tools.js | 13 +- .../zoom-out-mode-inserter-button.js | 47 + .../block-tools/zoom-out-mode-inserters.js | 63 +- .../block-tools/zoom-out-popover.js | 1 + .../global-styles/background-panel.js | 22 +- .../src/components/global-styles/hooks.js | 6 +- .../src/components/global-styles/style.scss | 5 + .../global-styles/test/typography-utils.js | 325 ++ .../test/use-global-styles-output.js | 3 +- .../global-styles/typography-panel.js | 80 +- .../global-styles/typography-utils.js | 110 +- .../global-styles/use-global-styles-output.js | 3 + packages/block-editor/src/components/index.js | 5 - .../inserter-draggable-blocks/index.js | 14 +- .../inserter/media-tab/media-panel.js | 1 + .../inserter/reusable-block-rename-hint.js | 69 - .../src/components/inserter/style.scss | 28 +- .../settings-tab-hint.js | 52 - .../inspector-controls-tabs/settings-tab.js | 2 - .../inspector-controls-tabs/style.scss | 21 - .../rich-text/format-toolbar/index.js | 2 +- .../src/components/rich-text/index.js | 2 +- .../block-editor/src/hooks/grid-visualizer.js | 4 +- .../src/hooks/use-bindings-attributes.js | 124 +- .../block-editor/src/layouts/constrained.js | 12 +- packages/block-editor/src/private-apis.js | 6 - packages/block-editor/src/store/actions.js | 15 + packages/block-editor/src/store/reducer.js | 18 + packages/block-editor/src/store/selectors.js | 10 + packages/block-library/src/block/edit.js | 85 +- .../src/block/test/edit.native.js | 8 +- packages/block-library/src/button/edit.js | 2 +- packages/block-library/src/button/index.php | 4 +- packages/block-library/src/buttons/block.json | 20 + .../block-library/src/categories/block.json | 8 + packages/block-library/src/categories/edit.js | 36 +- .../block-library/src/categories/editor.scss | 4 + .../block-library/src/categories/index.php | 5 +- .../block-library/src/categories/style.scss | 4 + packages/block-library/src/columns/index.js | 2 +- .../src/cover/edit/inspector-controls.js | 1 + packages/block-library/src/footnotes/index.js | 3 +- packages/block-library/src/form-input/edit.js | 2 + packages/block-library/src/gallery/block.json | 10 + packages/block-library/src/heading/block.json | 12 + packages/block-library/src/image/block.json | 3 + packages/block-library/src/image/edit.js | 2 +- packages/block-library/src/image/image.js | 6 +- packages/block-library/src/image/index.php | 47 +- packages/block-library/src/image/style.scss | 22 + .../src/image/test/edit.native.js | 1 + packages/block-library/src/image/view.js | 92 +- packages/block-library/src/list-item/edit.js | 4 +- packages/block-library/src/list/edit.js | 6 +- packages/block-library/src/list/style.scss | 2 +- .../block-library/src/media-text/block.json | 12 + .../src/navigation-submenu/edit.js | 14 +- .../block-library/src/navigation/index.php | 2 +- .../block-library/src/navigation/style.scss | 13 +- .../block-library/src/page-list-item/edit.js | 8 +- packages/block-library/src/page-list/edit.js | 1 + .../block-library/src/paragraph/block.json | 6 + .../edit/inspector-controls/author-control.js | 2 + .../query/edit/inspector-controls/index.js | 55 +- .../edit/inspector-controls/order-control.js | 1 + .../edit/inspector-controls/parent-control.js | 1 + .../edit/inspector-controls/sticky-control.js | 1 + .../inspector-controls/taxonomy-controls.js | 11 +- .../src/query/edit/query-content.js | 20 +- .../src/query/edit/query-placeholder.js | 22 +- packages/block-library/src/query/editor.scss | 9 - packages/block-library/src/quote/block.json | 16 +- packages/block-library/src/search/block.json | 3 + packages/block-library/src/site-logo/edit.js | 5 +- .../block-library/src/site-tagline/edit.js | 7 +- packages/block-library/src/site-title/edit.js | 5 +- .../src/social-link/socials-with-bg.scss | 2 +- .../src/social-link/socials-without-bg.scss | 2 +- .../block-library/src/social-links/block.json | 12 + packages/block-library/src/tag-cloud/edit.js | 8 +- .../src/template-part/edit/index.js | 8 +- .../src/template-part/edit/inner-blocks.js | 13 +- .../src/term-description/block.json | 12 + .../src/term-description/style.scss | 2 + packages/blocks/README.md | 4 + packages/blocks/package.json | 1 + packages/blocks/src/api/index.js | 19 + packages/blocks/src/api/registration.js | 179 +- packages/blocks/src/api/test/registration.js | 233 +- packages/blocks/src/store/private-actions.js | 25 +- .../blocks/src/store/process-block-type.js | 18 +- packages/blocks/src/store/reducer.js | 26 +- packages/components/CHANGELOG.md | 19 +- packages/components/CONTRIBUTING.md | 2 +- packages/components/src/button/README.md | 2 +- packages/components/src/button/index.tsx | 14 +- packages/components/src/button/test/index.tsx | 6 +- packages/components/src/button/types.ts | 11 +- packages/components/src/card/card/README.md | 2 +- .../components/src/checkbox-control/README.md | 8 + .../components/src/checkbox-control/index.tsx | 1 + .../components/src/color-picker/component.tsx | 34 +- .../components/src/combobox-control/README.md | 9 + .../components/src/combobox-control/index.tsx | 11 + .../src/combobox-control/test/index.tsx | 128 +- packages/components/src/disabled/README.md | 2 +- .../src/focal-point-picker/README.md | 9 + .../src/focal-point-picker/index.tsx | 1 + .../components/src/font-size-picker/README.md | 2 +- .../components/src/font-size-picker/index.tsx | 132 +- .../src/font-size-picker/test/index.tsx | 98 +- .../components/src/font-size-picker/types.ts | 9 +- .../components/src/form-token-field/README.md | 3 +- .../components/src/form-token-field/index.tsx | 9 + .../form-token-field/stories/index.story.tsx | 2 + .../navigation-screen.native.js | 2 +- .../notice/test/__snapshots__/index.tsx.snap | 2 +- .../resizable-box/resize-tooltip/README.md | 2 +- .../components/src/select-control/README.md | 8 + .../components/src/select-control/index.tsx | 7 + .../select-control/stories/index.story.tsx | 6 + .../styles/select-control-styles.ts | 24 +- .../components/src/select-control/types.ts | 6 + packages/components/src/tabs/styles.ts | 6 + .../components/src/textarea-control/README.md | 8 + .../components/src/textarea-control/index.tsx | 1 + .../test/__snapshots__/index.tsx.snap | 42 +- .../src/toggle-group-control/test/index.tsx | 114 + .../component.tsx | 7 +- .../styles.ts | 5 + .../tools-panel-header/component.tsx | 2 +- packages/components/src/tree-select/README.md | 9 + packages/components/src/tree-select/index.tsx | 1 + .../src/site-editor-navigation-commands.js | 5 +- packages/core-data/README.md | 41 + packages/core-data/src/entity-context.js | 6 + packages/core-data/src/entity-provider.js | 211 +- packages/core-data/src/hooks/index.ts | 3 + .../src/hooks/test/use-entity-record.js | 8 +- .../hooks/test/use-resource-permissions.js | 8 +- .../src/hooks/use-entity-block-editor.js | 148 + packages/core-data/src/hooks/use-entity-id.js | 21 + .../core-data/src/hooks/use-entity-prop.js | 60 + packages/core-data/src/resolvers.js | 69 +- packages/core-data/src/selectors.ts | 9 +- .../core-data/src/test/entity-provider.js | 8 +- packages/core-data/src/test/resolvers.js | 35 +- packages/core-data/src/utils/index.js | 5 + .../core-data/src/utils/user-permissions.js | 39 + .../components/sidebar-block-editor/index.js | 5 +- .../src/filters/move-to-sidebar.js | 2 +- .../src/filters/wide-widget-display.js | 2 +- packages/data/README.md | 2 +- .../components/registry-provider/context.js | 2 +- packages/dataviews/CHANGELOG.md | 6 +- packages/dataviews/README.md | 2 +- .../dataform/index.tsx} | 4 +- .../dataviews-bulk-actions-toolbar/index.tsx} | 72 +- .../dataviews-bulk-actions-toolbar/style.scss | 45 + .../dataviews-bulk-actions/index.tsx} | 62 +- .../dataviews-bulk-actions/style.scss | 7 + .../src/components/dataviews-context/index.ts | 47 + .../dataviews-filters}/add-filter.tsx | 6 +- .../dataviews-filters}/filter-summary.tsx | 28 +- .../dataviews-filters/index.tsx} | 32 +- .../dataviews-filters}/reset-filters.tsx | 2 +- .../dataviews-filters}/search-widget.tsx | 28 +- .../components/dataviews-filters/style.scss | 252 ++ .../dataviews-item-actions/index.tsx} | 4 +- .../dataviews-item-actions/style.scss | 3 + .../src/components/dataviews-layout/index.tsx | 49 + .../dataviews-pagination/index.tsx} | 34 +- .../dataviews-pagination/style.scss | 26 + .../dataviews-search/index.tsx} | 15 +- .../dataviews-selection-checkbox/index.tsx} | 18 +- .../dataviews-selection-checkbox/style.scss | 14 + .../dataviews-view-config/index.tsx} | 29 +- .../src/components/dataviews/index.tsx | 123 + .../src/components/dataviews/style.scss | 97 + packages/dataviews/src/dataviews.tsx | 183 - packages/dataviews/src/index.ts | 4 +- packages/dataviews/src/layouts/grid/index.tsx | 18 +- .../dataviews/src/layouts/grid/style.scss | 128 + packages/dataviews/src/layouts/list/index.tsx | 9 +- .../dataviews/src/layouts/list/style.scss | 189 + .../src/layouts/table/column-header-menu.tsx | 268 ++ .../dataviews/src/layouts/table/index.tsx | 234 +- .../dataviews/src/layouts/table/style.scss | 201 + packages/dataviews/src/style.scss | 962 +---- packages/dataviews/src/types.ts | 2 +- .../components/init-pattern-modal/index.js | 10 - .../edit-post/src/components/layout/index.js | 5 +- .../more-menu/manage-patterns-menu-item.js | 7 +- packages/edit-post/src/index.js | 2 + .../src/components/add-new-pattern/index.js | 10 +- .../src/components/add-new-template/index.js | 5 +- .../components/add-new-template/style.scss | 10 +- .../global-styles/block-preview-panel.js | 8 +- .../components/global-styles/font-families.js | 14 +- .../global-styles/font-library-modal/index.js | 6 +- .../font-library-modal/installed-fonts.js | 18 +- .../components/global-styles/screen-block.js | 7 +- .../style-variations-container.js | 4 + .../src/components/global-styles/style.scss | 2 +- .../src/components/layout/style.scss | 34 +- .../src/components/page-patterns/header.js | 2 +- .../src/components/page-patterns/index.js | 3 +- .../src/components/page-templates/index.js | 21 +- .../src/components/post-list/index.js | 17 +- .../src/components/sidebar-dataviews/index.js | 37 +- .../push-changes-to-global-styles/index.js | 2 +- packages/edit-site/src/index.js | 4 +- .../index.js | 7 +- .../src/filters/move-to-widget-area.js | 2 +- packages/editor/CHANGELOG.md | 4 + packages/editor/README.md | 3 +- packages/editor/src/bindings/api.js | 27 + packages/editor/src/bindings/index.js | 15 - .../editor/src/bindings/pattern-overrides.js | 43 +- packages/editor/src/bindings/post-meta.js | 21 +- .../content-only-settings-menu.js | 8 +- .../src/components/page-attributes/order.js | 80 +- .../src/components/page-attributes/panel.js | 8 +- .../src/components/post-actions/actions.js | 125 +- .../src/components/post-excerpt/panel.js | 3 +- .../components/post-last-revision/index.js | 1 + .../src/components/post-panel-row/style.scss | 1 - .../maybe-category-panel.js | 5 +- .../post-publish-panel/maybe-tags-panel.js | 2 +- .../src/components/post-status/index.js | 28 +- .../post-taxonomies/flat-term-selector.js | 20 +- .../src/components/post-taxonomies/index.js | 9 +- .../components/post-template/block-theme.js | 6 +- .../components/post-template/classic-theme.js | 11 +- .../post-template/create-new-template.js | 5 +- .../src/components/post-template/panel.js | 12 +- .../editor/src/components/post-trash/check.js | 14 +- .../provider/use-block-editor-settings.js | 16 +- .../edit-template-blocks-notification.js | 6 +- .../src/components/visual-editor/index.js | 5 +- .../editor/src/hooks/pattern-overrides.js | 2 +- packages/editor/src/index.js | 1 - packages/editor/src/private-apis.js | 2 + packages/editor/src/store/test/actions.js | 12 +- packages/editor/src/utils/set-nested-value.js | 39 - packages/eslint-plugin/configs/es5.js | 8 +- packages/icons/src/library/close.js | 2 +- .../components/complementary-area/style.scss | 2 +- .../dot-tip/test/__snapshots__/index.js.snap | 2 +- .../src/components/pattern-convert-button.js | 5 +- .../pattern-overrides-block-controls.js | 2 +- .../src/components/patterns-manage-button.js | 15 +- packages/react-native-aztec/package.json | 2 +- packages/react-native-bridge/package.json | 2 +- packages/react-native-editor/CHANGELOG.md | 2 + packages/react-native-editor/ios/Podfile.lock | 8 +- packages/react-native-editor/package.json | 2 +- .../reusable-block-convert-button.js | 21 +- .../reusable-blocks-manage-button.js | 15 +- packages/scripts/CHANGELOG.md | 4 + packages/scripts/README.md | 4 +- packages/scripts/config/webpack.config.js | 38 +- packages/scripts/package.json | 3 +- packages/scripts/scripts/check-licenses.js | 31 +- packages/scripts/utils/config.js | 36 +- packages/scripts/utils/index.js | 4 +- .../style-engine/class-wp-style-engine.php | 14 +- .../src/styles/background/index.ts | 13 + packages/style-engine/src/test/index.js | 6 + phpunit/block-supports/background-test.php | 11 +- phpunit/class-wp-theme-json-test.php | 49 +- phpunit/style-engine/style-engine-test.php | 20 +- schemas/json/theme.json | 33 +- schemas/json/wp-env.json | 17 +- test/e2e/specs/editor/blocks/image.spec.js | 31 + .../various/font-appearance-control.spec.js | 90 + .../specs/editor/various/is-typing.spec.js | 3 + .../specs/editor/various/rich-text.spec.js | 4 +- .../site-editor/navigation-editor.spec.js | 8 +- .../fixtures/blocks/core__categories.json | 3 +- .../helpers/integration-test-editor.js | 11 +- tools/webpack/packages.js | 15 +- tools/webpack/vendors.js | 11 +- 340 files changed, 16326 insertions(+), 5427 deletions(-) create mode 100644 backport-changelog/6.6/7036.md create mode 100644 backport-changelog/6.6/7061.md create mode 100644 backport-changelog/6.7/6991.md rename bin/api-docs/{gen-theme-reference.js => gen-theme-reference.mjs} (82%) create mode 100644 lib/compat/wordpress-6.7/class-gutenberg-token-map-6-7.php create mode 100644 lib/compat/wordpress-6.7/html-api/class-gutenberg-html-active-formatting-elements-6-7.php create mode 100644 lib/compat/wordpress-6.7/html-api/class-gutenberg-html-attribute-token-6-7.php create mode 100644 lib/compat/wordpress-6.7/html-api/class-gutenberg-html-decoder-6-7.php create mode 100644 lib/compat/wordpress-6.7/html-api/class-gutenberg-html-open-elements-6-7.php create mode 100644 lib/compat/wordpress-6.7/html-api/class-gutenberg-html-processor-6-7.php create mode 100644 lib/compat/wordpress-6.7/html-api/class-gutenberg-html-processor-state-6-7.php create mode 100644 lib/compat/wordpress-6.7/html-api/class-gutenberg-html-span-6-7.php create mode 100644 lib/compat/wordpress-6.7/html-api/class-gutenberg-html-stack-event-6-7.php create mode 100644 lib/compat/wordpress-6.7/html-api/class-gutenberg-html-tag-processor-6-7.php create mode 100644 lib/compat/wordpress-6.7/html-api/class-gutenberg-html-text-replacement-6-7.php create mode 100644 lib/compat/wordpress-6.7/html-api/class-gutenberg-html-token-6-7.php create mode 100644 lib/compat/wordpress-6.7/html-api/class-gutenberg-html-unsupported-exception-6-7.php create mode 100644 packages/block-editor/src/components/block-tools/zoom-out-mode-inserter-button.js delete mode 100644 packages/block-editor/src/components/inserter/reusable-block-rename-hint.js delete mode 100644 packages/block-editor/src/components/inspector-controls-tabs/settings-tab-hint.js create mode 100644 packages/core-data/src/entity-context.js create mode 100644 packages/core-data/src/hooks/use-entity-block-editor.js create mode 100644 packages/core-data/src/hooks/use-entity-id.js create mode 100644 packages/core-data/src/hooks/use-entity-prop.js create mode 100644 packages/core-data/src/utils/user-permissions.js rename packages/dataviews/src/{dataform.tsx => components/dataform/index.tsx} (94%) rename packages/dataviews/src/{bulk-actions-toolbar.tsx => components/dataviews-bulk-actions-toolbar/index.tsx} (78%) create mode 100644 packages/dataviews/src/components/dataviews-bulk-actions-toolbar/style.scss rename packages/dataviews/src/{bulk-actions.tsx => components/dataviews-bulk-actions/index.tsx} (85%) create mode 100644 packages/dataviews/src/components/dataviews-bulk-actions/style.scss create mode 100644 packages/dataviews/src/components/dataviews-context/index.ts rename packages/dataviews/src/{ => components/dataviews-filters}/add-filter.tsx (92%) rename packages/dataviews/src/{ => components/dataviews-filters}/filter-summary.tsx (92%) rename packages/dataviews/src/{filters.tsx => components/dataviews-filters/index.tsx} (75%) rename packages/dataviews/src/{ => components/dataviews-filters}/reset-filters.tsx (94%) rename packages/dataviews/src/{ => components/dataviews-filters}/search-widget.tsx (88%) create mode 100644 packages/dataviews/src/components/dataviews-filters/style.scss rename packages/dataviews/src/{item-actions.tsx => components/dataviews-item-actions/index.tsx} (98%) create mode 100644 packages/dataviews/src/components/dataviews-item-actions/style.scss create mode 100644 packages/dataviews/src/components/dataviews-layout/index.tsx rename packages/dataviews/src/{pagination.tsx => components/dataviews-pagination/index.tsx} (79%) create mode 100644 packages/dataviews/src/components/dataviews-pagination/style.scss rename packages/dataviews/src/{search.tsx => components/dataviews-search/index.tsx} (77%) rename packages/dataviews/src/{single-selection-checkbox.tsx => components/dataviews-selection-checkbox/index.tsx} (75%) create mode 100644 packages/dataviews/src/components/dataviews-selection-checkbox/style.scss rename packages/dataviews/src/{view-actions.tsx => components/dataviews-view-config/index.tsx} (91%) create mode 100644 packages/dataviews/src/components/dataviews/index.tsx create mode 100644 packages/dataviews/src/components/dataviews/style.scss delete mode 100644 packages/dataviews/src/dataviews.tsx create mode 100644 packages/dataviews/src/layouts/grid/style.scss create mode 100644 packages/dataviews/src/layouts/list/style.scss create mode 100644 packages/dataviews/src/layouts/table/column-header-menu.tsx create mode 100644 packages/dataviews/src/layouts/table/style.scss create mode 100644 packages/editor/src/bindings/api.js delete mode 100644 packages/editor/src/bindings/index.js delete mode 100644 packages/editor/src/utils/set-nested-value.js create mode 100644 test/e2e/specs/editor/various/font-appearance-control.spec.js diff --git a/.eslintrc.js b/.eslintrc.js index 7925bceafd3d5..4385f528f44db 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -277,6 +277,34 @@ module.exports = { ], }, }, + { + // Temporary rules until we're ready to officially deprecate the bottom margins. + files: [ 'packages/*/src/**/*.[tj]s?(x)' ], + excludedFiles: [ + 'packages/components/src/**/@(test|stories)/**', + '**/*.@(native|ios|android).js', + ], + rules: { + 'no-restricted-syntax': [ + 'error', + ...restrictedSyntax, + ...restrictedSyntaxComponents, + ...[ + 'CheckboxControl', + 'ComboboxControl', + 'FocalPointPicker', + 'SearchControl', + 'TextareaControl', + 'TreeSelect', + ].map( ( componentName ) => ( { + selector: `JSXOpeningElement[name.name="${ componentName }"]:not(:has(JSXAttribute[name.name="__nextHasNoMarginBottom"]))`, + message: + componentName + + ' should have the `__nextHasNoMarginBottom` prop to opt-in to the new margin-free styles.', + } ) ), + ], + }, + }, { files: [ // Components package. diff --git a/.gitattributes b/.gitattributes index 6c72e80a40297..1dc48620d8b67 100644 --- a/.gitattributes +++ b/.gitattributes @@ -20,3 +20,6 @@ changelog.txt linguist-language=Markdown # Flag docs directory as documentation for GitHub stats. docs/** linguist-documentation + +# TSConfig files use jsonc. +tsconfig*.json linguist-language=jsonc diff --git a/.github/ISSUE_TEMPLATE/Bug_report.yml b/.github/ISSUE_TEMPLATE/Bug_report.yml index 1109056e7e5d5..5d7c876ccefca 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.yml +++ b/.github/ISSUE_TEMPLATE/Bug_report.yml @@ -49,25 +49,19 @@ body: validations: required: false - - type: dropdown + - type: checkboxes id: existing attributes: label: Please confirm that you have searched existing issues in the repo. description: You can do this by searching https://github.com/WordPress/gutenberg/issues and making sure the bug is not related to another plugin. - multiple: true options: - - 'Yes' - - 'No' - validations: - required: true + - label: 'Yes' + required: true - - type: dropdown + - type: checkboxes id: plugins attributes: label: Please confirm that you have tested with all plugins deactivated except Gutenberg. - multiple: true options: - - 'Yes' - - 'No' - validations: - required: true + - label: 'Yes' + required: true diff --git a/.github/workflows/create-block.yml b/.github/workflows/create-block.yml index 0de1b9ee6566a..245b136ee22c1 100644 --- a/.github/workflows/create-block.yml +++ b/.github/workflows/create-block.yml @@ -14,13 +14,17 @@ concurrency: jobs: checks: - name: Checks w/Node.js ${{ matrix.node }} on ${{ matrix.os }} + name: Checks w/Node.js ${{ matrix.node.name }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} strategy: fail-fast: false matrix: - node: ['20', '22'] + node: + - name: 20 + version: 20 + - name: 22 + version: 22.4 os: ['macos-latest', 'ubuntu-latest', 'windows-latest'] steps: @@ -31,7 +35,7 @@ jobs: - name: Setup Node.js and install dependencies uses: ./.github/setup-node with: - node-version: ${{ matrix.node }} + node-version: ${{ matrix.node.version }} - name: Create block shell: bash diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 488f41c217e7c..75a0d34ddc8d8 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -21,13 +21,17 @@ concurrency: jobs: unit-js: - name: JavaScript (Node.js ${{ matrix.node }}) ${{ matrix.shard }} + name: JavaScript (Node.js ${{ matrix.node.name }}) ${{ matrix.shard }} runs-on: ubuntu-latest if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} strategy: fail-fast: false matrix: - node: ['20', '22'] + node: + - name: 20 + version: 20 + - name: 22 + version: 22.4 shard: ['1/4', '2/4', '3/4', '4/4'] steps: @@ -39,7 +43,7 @@ jobs: - name: Setup Node.js and install dependencies uses: ./.github/setup-node with: - node-version: ${{ matrix.node }} + node-version: ${{ matrix.node.version }} - name: Determine the number of CPU cores uses: SimenB/github-actions-cpu-cores@97ba232459a8e02ff6121db9362b09661c875ab8 # v2.0.0 @@ -60,13 +64,17 @@ jobs: --cacheDirectory="$HOME/.jest-cache" unit-js-date: - name: JavaScript Date Tests (Node.js ${{ matrix.node }}) + name: JavaScript Date Tests (Node.js ${{ matrix.node.name }}) runs-on: ubuntu-latest if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} strategy: fail-fast: false matrix: - node: ['20', '22'] + node: + - name: 20 + version: 20 + - name: 22 + version: 22.4 steps: - name: Checkout repository @@ -77,7 +85,7 @@ jobs: - name: Setup Node.js and install dependencies uses: ./.github/setup-node with: - node-version: ${{ matrix.node }} + node-version: ${{ matrix.node.version }} - name: Determine the number of CPU cores uses: SimenB/github-actions-cpu-cores@97ba232459a8e02ff6121db9362b09661c875ab8 # v2.0.0 diff --git a/backport-changelog/6.6/7036.md b/backport-changelog/6.6/7036.md new file mode 100644 index 0000000000000..afc4d16bf011b --- /dev/null +++ b/backport-changelog/6.6/7036.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7036 + +* https://github.com/WordPress/gutenberg/pull/63436 diff --git a/backport-changelog/6.6/7061.md b/backport-changelog/6.6/7061.md new file mode 100644 index 0000000000000..307e6575cf38d --- /dev/null +++ b/backport-changelog/6.6/7061.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7061 + +* https://github.com/WordPress/gutenberg/pull/63726 diff --git a/backport-changelog/6.7/6991.md b/backport-changelog/6.7/6991.md new file mode 100644 index 0000000000000..4d5f1f85ec768 --- /dev/null +++ b/backport-changelog/6.7/6991.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/6991 + +* https://github.com/WordPress/gutenberg/pull/61382 diff --git a/bin/api-docs/gen-theme-reference.js b/bin/api-docs/gen-theme-reference.mjs similarity index 82% rename from bin/api-docs/gen-theme-reference.js rename to bin/api-docs/gen-theme-reference.mjs index 07a8c2fcc697d..051f23c3ea997 100644 --- a/bin/api-docs/gen-theme-reference.js +++ b/bin/api-docs/gen-theme-reference.mjs @@ -7,8 +7,13 @@ /** * External dependencies */ -const path = require( 'path' ); -const fs = require( 'fs' ); +import path from 'path'; +import fs from 'fs'; +import url from 'url'; +import $RefParser from '@apidevtools/json-schema-ref-parser'; + +const __dirname = path.dirname( url.fileURLToPath( import.meta.url ) ); + /** * Path to root project directory. * @@ -58,7 +63,7 @@ const END_TOKEN = ''; */ const TOKEN_PATTERN = new RegExp( START_TOKEN + '[^]*' + END_TOKEN ); -const themejson = require( THEME_JSON_SCHEMA_FILE ); +const themejson = await $RefParser.dereference( THEME_JSON_SCHEMA_FILE ); /** * Convert object keys to an array. @@ -74,42 +79,6 @@ const keys = ( maybeObject ) => { return Object.keys( maybeObject ); }; -/** - * Get definition from ref. - * - * @param {string} ref - * @return {Object} definition - * @throws {Error} If the referenced definition is not found in 'themejson.definitions'. - * - * @example - * getDefinition( '#/definitions/typographyProperties/properties/fontFamily' ) - * // returns themejson.definitions.typographyProperties.properties.fontFamily - */ -const resolveDefinitionRef = ( ref ) => { - const refParts = ref.split( '/' ); - const definition = refParts[ refParts.length - 1 ]; - if ( ! themejson.definitions[ definition ] ) { - throw new Error( `Can't resolve '${ ref }'. Definition not found` ); - } - return themejson.definitions[ definition ]; -}; - -/** - * Get properties from an array. - * - * @param {Object} items - * @return {Object} properties - */ -const getPropertiesFromArray = ( items ) => { - // if its a $ref resolve it - if ( items.$ref ) { - return resolveDefinitionRef( items.$ref ).properties; - } - - // otherwise just return the properties - return items.properties; -}; - /** * Convert settings properties to markup. * @@ -133,9 +102,7 @@ const getSettingsPropertiesMarkup = ( struct ) => { let type = props[ key ].type || ''; let ps = props[ key ].type === 'array' - ? keys( getPropertiesFromArray( props[ key ].items ) ) - .sort() - .join( ', ' ) + ? keys( props[ key ].items.properties ).sort().join( ', ' ) : ''; /* @@ -154,9 +121,7 @@ const getSettingsPropertiesMarkup = ( struct ) => { .map( ( item ) => item?.type === 'object' && item?.properties ? '_{' + - keys( getPropertiesFromArray( item ) ) - .sort() - .join( ', ' ) + + keys( item.properties ).sort().join( ', ' ) + '}_' : '' ) @@ -244,10 +209,6 @@ const formatType = ( prop ) => { if ( item.type ) { types.push( item.type ); } - // refComplete is always an object - if ( item.$ref && item.$ref === '#/definitions/refComplete' ) { - types.push( 'object' ); - } } ); type = [ ...new Set( types ) ].join( ', ' ); diff --git a/changelog.txt b/changelog.txt index 552c14cdd1715..8e63d9a2d1f44 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,7 +1,6 @@ == Changelog == -= 18.8.0-rc.1 = - += 18.8.0 = ## Changelog @@ -139,6 +138,7 @@ - Fix Incorrect URL basename logic in EmbedPreview. ([63052](https://github.com/WordPress/gutenberg/pull/63052)) - Fix: Update "Link Text" label to "Text" on Social Icons block #60966. ([61715](https://github.com/WordPress/gutenberg/pull/61715)) - List: Maintain nested list on parent item removal. ([62949](https://github.com/WordPress/gutenberg/pull/62949)) +- Navigation: Allow themes to override block library text-decoration rule. ([63406](https://github.com/WordPress/gutenberg/pull/63406)) - Patterns: Check for edited entity content property when exporting. ([63227](https://github.com/WordPress/gutenberg/pull/63227)) - Reduce specificity of social link icon specific colors. ([63049](https://github.com/WordPress/gutenberg/pull/63049)) - Refactor Post Date Relative Time Rendering for Future Dates. ([62979](https://github.com/WordPress/gutenberg/pull/62979)) @@ -171,11 +171,14 @@ - Only hide drop indicator when grid has `isManualPlacement` set. ([63226](https://github.com/WordPress/gutenberg/pull/63226)) - Remove dotted border from grid dropzone. ([63162](https://github.com/WordPress/gutenberg/pull/63162)) - Resizing in Auto mode shouldn't add `columnStart` and `rowStart` values. ([63160](https://github.com/WordPress/gutenberg/pull/63160)) +- Fix invalid css for nested fullwidth layouts with zero padding applied. ([63436](https://github.com/WordPress/gutenberg/pull/63436)) #### Global Styles +- Elements: Avoid specificity bump for top-level element-only selectors. ([63403](https://github.com/WordPress/gutenberg/pull/63403)) - Global styles revisions: Ensure that user-defined variation styles CSS is generated. ([62768](https://github.com/WordPress/gutenberg/pull/62768)) - Root padding styles: Include alignwide in nested has-outer-padding logic. ([63207](https://github.com/WordPress/gutenberg/pull/63207)) - Remove letter-spacing from typography element preview. ([60322](https://github.com/WordPress/gutenberg/pull/60322)) +- Only add customizer additional CSS to global styles in block themes. ([63331](https://github.com/WordPress/gutenberg/pull/63331)) #### Site Editor - Make SiteHub available for Pages, Patterns, and Templates in mobile viewports. ([63118](https://github.com/WordPress/gutenberg/pull/63118)) @@ -190,6 +193,7 @@ #### Block Editor - Featured Image Panel: Align text and icons horizontally to avoid clipping. ([62842](https://github.com/WordPress/gutenberg/pull/62842)) - Zoom Out: Move the hook to the inserter component. ([63315](https://github.com/WordPress/gutenberg/pull/63315)) +- Fix error when calling the PostActions `view-post` callback. ([63460](https://github.com/WordPress/gutenberg/pull/63460)) #### Block bindings - Disable post meta editing in blocks inside a Query Loop. ([63237](https://github.com/WordPress/gutenberg/pull/63237)) @@ -212,6 +216,7 @@ #### Typography - Use available font weights and styles in FontAppearanceControl. ([61915](https://github.com/WordPress/gutenberg/pull/61915)) +- Font Appearance Control: Refactor font appearance fallbacks. ([63215](https://github.com/WordPress/gutenberg/pull/63215)) ### Accessibility @@ -366,6 +371,7 @@ The following PRs were merged by first-time contributors: - @iworks: Translation should depend on number of items. ([62857](https://github.com/WordPress/gutenberg/pull/62857)) - @roygbyte: Fix typo to be preposition, not verb, in some package comments and documentations. ([62945](https://github.com/WordPress/gutenberg/pull/62945)) - @shreya0204: Add justification to block toolbar in addition to sidebar. ([62924](https://github.com/WordPress/gutenberg/pull/62924)) +- @sejas: Fix: Error when calling the PostActions `view-post` callback. ([63460](https://github.com/WordPress/gutenberg/pull/63460)) ## Contributors @@ -375,6 +381,8 @@ The following contributors merged PRs in this release: @aaronrobertshaw @afercia @airman5573 @akasunil @aliaghdam @amitraj2203 @bogiii @carolinan @Chrico @ciampo @costasovo @creativecoder @DaniGuardiola @desrosj @dhananjaykuber @dmsnell @ellatrix @fluiddot @geriux @hbhalodia @iamibrahimriaz @iworks @jameskoster @jasmussen @jeryj @jffng @jorgefilipecosta @jsnajdr @juanmaguitar @kevin940726 @luisherranz @MaggieCabrera @Mamaduka @matiasbenedetto @michalczaplinski @mikachan @mirka @ndiego @ntsekouras @oandregal @ockham @peterwilsoncc @ramonjd @richtabor @roygbyte @SantosGuillamot @scruffian @shail-mehta @shreya0204 @sirreal @stokesman @swissspidy @t-hamano @talldan @tellthemachines @tyxla @vipul0425 @westonruter @youknowriad + + = 18.7.1 = diff --git a/docs/contributors/code/coding-guidelines.md b/docs/contributors/code/coding-guidelines.md index 0365a87e0ae40..d8a5220c70ca2 100644 --- a/docs/contributors/code/coding-guidelines.md +++ b/docs/contributors/code/coding-guidelines.md @@ -65,7 +65,7 @@ Examples of styles that appear in both the theme and the editor include gallery ## JavaScript -JavaScript in Gutenberg uses modern language features of the [ECMAScript language specification](https://www.ecma-international.org/ecma-262/) as well as the [JSX language syntax extension](https://reactjs.org/docs/introducing-jsx.html). These are enabled through a combination of preset configurations, notably [`@wordpress/babel-preset-default`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/babel-preset-default) which is used as a preset in the project's [Babel](https://babeljs.io/) configuration. +JavaScript in Gutenberg uses modern language features of the [ECMAScript language specification](https://www.ecma-international.org/ecma-262/) as well as the [JSX language syntax extension](https://react.dev/learn/writing-markup-with-jsx). These are enabled through a combination of preset configurations, notably [`@wordpress/babel-preset-default`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/babel-preset-default) which is used as a preset in the project's [Babel](https://babeljs.io/) configuration. While the [staged process](https://tc39.es/process-document/) for introducing a new JavaScript language feature offers an opportunity to use new features before they are considered complete, **the Gutenberg project and the `@wordpress/babel-preset-default` configuration will only target support for proposals which have reached Stage 4 ("Finished")**. @@ -531,7 +531,7 @@ alert( `My name is ${ name }.` ); ### React components -It is preferred to implement all components as [function components](https://reactjs.org/docs/components-and-props.html), using [hooks](https://reactjs.org/docs/hooks-reference.html) to manage component state and lifecycle. With the exception of [error boundaries](https://reactjs.org/docs/error-boundaries.html), you should never encounter a situation where you must use a class component. Note that the [WordPress guidance on Code Refactoring](https://make.wordpress.org/core/handbook/contribute/code-refactoring/) applies here: There needn't be a concentrated effort to update class components in bulk. Instead, consider it as a good refactoring opportunity in combination with some other change. +It is preferred to implement all components as [function components](https://react.dev/learn/your-first-component), using [hooks](https://react.dev/reference/react/hooks) to manage component state and lifecycle. With the exception of [error boundaries](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary), you should never encounter a situation where you must use a class component. Note that the [WordPress guidance on Code Refactoring](https://make.wordpress.org/core/handbook/contribute/code-refactoring/) applies here: There needn't be a concentrated effort to update class components in bulk. Instead, consider it as a good refactoring opportunity in combination with some other change. ## JavaScript documentation using JSDoc @@ -758,7 +758,7 @@ When documenting an example, use the markdown \`\`\` code block to ### Documenting React components -When possible, all components should be implemented as [function components](https://reactjs.org/docs/components-and-props.html#function-and-class-components), using [hooks](https://react.dev/reference/react/hooks) for managing component lifecycle and state. +When possible, all components should be implemented as [function components](https://react.dev/learn/your-first-component), using [hooks](https://react.dev/reference/react/hooks) for managing component lifecycle and state. Documenting a function component should be treated the same as any other function. The primary caveat in documenting a component is being aware that the function typically accepts only a single argument (the "props"), which may include many property members. Use the [dot syntax for parameter properties](https://jsdoc.app/tags-param.html#parameters-with-properties) to document individual prop types. diff --git a/docs/contributors/code/deprecations.md b/docs/contributors/code/deprecations.md index 7463a99b319d5..51f0165c409da 100644 --- a/docs/contributors/code/deprecations.md +++ b/docs/contributors/code/deprecations.md @@ -183,7 +183,7 @@ For features included in the Gutenberg plugin, the deprecation policy is intende ## 3.8.0 -- `wp.components.withContext` has been removed. Please use `wp.element.createContext` instead. See: https://reactjs.org/docs/context.html. +- `wp.components.withContext` has been removed. Please use `wp.element.createContext` instead. See: https://react.dev/reference/react/createContext. - `wp.coreBlocks.registerCoreBlocks` has been removed. Please use `wp.blockLibrary.registerCoreBlocks` instead. - `wp.editor.DocumentTitle` component has been removed. - `getDocumentTitle` selector (`core/editor`) has been removed. diff --git a/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md b/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md index 8a0d172e45f45..955bc1f869ad8 100644 --- a/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md +++ b/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md @@ -172,7 +172,7 @@ Note that instead of using an `input` tag, we took advantage of the [SearchContr ![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/filter-field.jpg) -The field starts empty, and the contents are stored in the `searchTerm` state value. If you aren’t familiar with the [useState](https://reactjs.org/docs/hooks-state.html) hook, you can learn more in [React’s documentation](https://reactjs.org/docs/hooks-state.html). +The field starts empty, and the contents are stored in the `searchTerm` state value. If you aren’t familiar with the [useState](https://react.dev/reference/react/useState) hook, you can learn more in [React’s documentation](https://react.dev/reference/react/useState). We can now request only the pages matching the `searchTerm`. diff --git a/docs/how-to-guides/platform/custom-block-editor.md b/docs/how-to-guides/platform/custom-block-editor.md index a7abb00adacec..e100820ea0e8b 100644 --- a/docs/how-to-guides/platform/custom-block-editor.md +++ b/docs/how-to-guides/platform/custom-block-editor.md @@ -277,7 +277,7 @@ function Editor( { settings } ) { } ``` -In this process, the core of the editor's layout is being scaffolded, along with a few specialized [context providers](https://reactjs.org/docs/context.html#contextprovider) that make specific functionality available throughout the component hierarchy. +In this process, the core of the editor's layout is being scaffolded, along with a few specialized [context providers](https://react.dev/reference/react/createContext#provider) that make specific functionality available throughout the component hierarchy. Let's examine these in more detail: diff --git a/docs/reference-guides/block-api/block-context.md b/docs/reference-guides/block-api/block-context.md index abc956e587477..5fdc670fe6040 100644 --- a/docs/reference-guides/block-api/block-context.md +++ b/docs/reference-guides/block-api/block-context.md @@ -4,7 +4,7 @@ Block context is a feature which enables ancestor blocks to provide values which This is especially useful in full-site editing where, for example, the contents of a block may depend on the context of the post in which it is displayed. A blogroll template may show excerpts of many different posts. Using block context, there can still be one single "Post Excerpt" block which displays the contents of the post based on an inherited post ID. -If you are familiar with [React Context](https://reactjs.org/docs/context.html), block context adopts many of the same ideas. In fact, the client-side block editor implementation of block context is a very simple application of React Context. Block context is also supported in server-side `render_callback` implementations, demonstrated in the examples below. +If you are familiar with [React Context](https://react.dev/learn/passing-data-deeply-with-context), block context adopts many of the same ideas. In fact, the client-side block editor implementation of block context is a very simple application of React Context. Block context is also supported in server-side `render_callback` implementations, demonstrated in the examples below. ## Defining block context diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 67cebe47fc41b..0b6e6aa3fd676 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -61,7 +61,7 @@ Prompt visitors to take action with a group of button-style links. ([Source](htt - **Name:** core/buttons - **Category:** design - **Allowed Blocks:** core/button -- **Supports:** align (full, wide), anchor, interactivity (clientNavigation), layout (default, ~~allowInheriting~~, ~~allowSwitching~~), spacing (blockGap, margin), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align (full, wide), anchor, color (background, gradients, ~~text~~), interactivity (clientNavigation), layout (default, ~~allowInheriting~~, ~~allowSwitching~~), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~ ## Calendar @@ -79,7 +79,7 @@ Display a list of all categories. ([Source](https://github.com/WordPress/gutenbe - **Name:** core/categories - **Category:** widgets - **Supports:** align, interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ -- **Attributes:** displayAsDropdown, showEmpty, showHierarchy, showOnlyTopLevel, showPostCounts +- **Attributes:** displayAsDropdown, label, showEmpty, showHierarchy, showLabel, showOnlyTopLevel, showPostCounts ## Code @@ -387,7 +387,7 @@ Insert an image to make a visual statement. ([Source](https://github.com/WordPre - **Name:** core/image - **Category:** media -- **Supports:** align (center, full, left, right, wide), anchor, color (~~background~~, ~~text~~), filter (duotone), interactivity, shadow () +- **Supports:** align (center, full, left, right, wide), anchor, color (~~background~~, ~~text~~), filter (duotone), interactivity, shadow (), spacing (margin) - **Attributes:** alt, aspectRatio, blob, caption, height, href, id, lightbox, linkClass, linkDestination, linkTarget, rel, scale, sizeSlug, title, url, width ## Latest Comments @@ -783,7 +783,7 @@ Give quoted text visual emphasis. "In quoting others, we cite ourselves." — Ju - **Name:** core/quote - **Category:** text -- **Supports:** anchor, background (backgroundImage, backgroundSize), color (background, gradients, heading, link, text), dimensions (minHeight), interactivity (clientNavigation), layout (~~allowEditing~~), spacing (blockGap), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** anchor, background (backgroundImage, backgroundSize), color (background, gradients, heading, link, text), dimensions (minHeight), interactivity (clientNavigation), layout (~~allowEditing~~), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** citation, textAlign, value ## Read More @@ -810,7 +810,7 @@ Help visitors find your content. ([Source](https://github.com/WordPress/gutenber - **Name:** core/search - **Category:** widgets -- **Supports:** align (center, left, right), color (background, gradients, text), interactivity, typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align (center, left, right), color (background, gradients, text), interactivity, spacing (margin), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** buttonPosition, buttonText, buttonUseIcon, isSearchFieldHidden, label, placeholder, query, showLabel, width, widthUnit ## Separator diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 4b66ad9eb6cb4..7eed5c8741288 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -562,6 +562,18 @@ _Returns_ - `number`: Number of blocks in the post, or number of blocks with name equal to blockName. +### getHoveredBlockClientId + +Returns the currently hovered block. + +_Parameters_ + +- _state_ `Object`: Global application state. + +_Returns_ + +- `Object`: Client Id of the hovered block. + ### getInserterItems Determines the items that appear in the inserter. Includes both static items (e.g. a regular block type) and dynamic items (e.g. a reusable block). @@ -1257,6 +1269,18 @@ _Parameters_ Action that hides the insertion point. +### hoverBlock + +Returns an action object used in signalling that the block with the specified client ID has been hovered. + +_Parameters_ + +- _clientId_ `string`: Block client ID. + +_Returns_ + +- `Object`: Action object. + ### insertAfterBlock Action that inserts a default block after a given block. diff --git a/docs/reference-guides/filters/editor-filters.md b/docs/reference-guides/filters/editor-filters.md index a27a989dbc788..08d1c5ddb8a33 100644 --- a/docs/reference-guides/filters/editor-filters.md +++ b/docs/reference-guides/filters/editor-filters.md @@ -226,9 +226,9 @@ function example_filter_block_editor_rest_api_preload_paths_when_post_provided( ## Logging errors -A JavaScript error in a part of the UI shouldn't break the whole app. To solve this problem for users, React library uses the concept of an ["error boundary"](https://reactjs.org/docs/error-boundaries.html). Error boundaries are React components that catch JavaScript errors anywhere in their child component tree and display a fallback UI instead of the component tree that crashed. +A JavaScript error in a part of the UI shouldn't break the whole app. To solve this problem for users, React library uses the concept of an ["error boundary"](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary). Error boundaries are React components that catch JavaScript errors anywhere in their child component tree and display a fallback UI instead of the component tree that crashed. -The `editor.ErrorBoundary.errorLogged` action allows you to hook into the [Error Boundaries](https://reactjs.org/docs/error-boundaries.html) and gives you access to the error object. +The `editor.ErrorBoundary.errorLogged` action allows you to hook into the [Error Boundaries](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) and gives you access to the error object. You can use this action to get hold of the error object handled by the boundaries. For example, you may want to send them to an external error-tracking tool. Here's an example: diff --git a/docs/reference-guides/interactivity-api/iapi-faq.md b/docs/reference-guides/interactivity-api/iapi-faq.md index fba3603f39169..5e5036779baae 100644 --- a/docs/reference-guides/interactivity-api/iapi-faq.md +++ b/docs/reference-guides/interactivity-api/iapi-faq.md @@ -33,6 +33,15 @@ React was considered first because Gutenberg developers are familiar with it. Ot Alpine.js is a great framework, and it inspired a lot of functionality in the Interactivity API. However, it doesn’t support server-side rendering of its [directives](https://github.com/alpinejs/alpine/tree/d7f9d641f7a763c56c598d118bd189a406a22383/packages/docs/src/en/directives), and having a similar system tailored for WordPress blocks has many benefits. +Preact was chosen instead of Alpine.js for numerous reasons, such as its smaller size, its better performance (especially with the addition of [signals](https://preactjs.com/guide/v10/signals/)), the fact that custom directives are written with Preact’s declarative syntax and tooling (hooks, signals), it’s more battle-tested and has a larger community than Alpine.js. It’s also compatible with React (for sharing client-side rendered components from the Editor), and it provides to the Interactivity API the fastest DOM diffing algorithm out of the box, including UI state preservation. + +Furthermore, with Preact operating in the background, the Interactivity API manages "the final layer" so it can be better adapted to WordPress requirements. For example, JavaScript expressions are not allowed inside directives to avoid security risks and ensure compliance with strict security policies, and all WordPress directives are spec-compliant HTML attributes. + +
+ Have a look at the conversation at "Why Preact instead of Alpine?" to learn more about this. +
+ + ### Plain JavaScript See the answer below. diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index cf9ea9bc39113..d3f0b111d994d 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -216,6 +216,7 @@ Background styles. | backgroundPosition | string, object | | | backgroundRepeat | string, object | | | backgroundSize | string, object | | +| backgroundAttachment | string, object | | --- diff --git a/gutenberg.php b/gutenberg.php index b59d9a548a4e1..0d21366bcc29d 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * 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 PHP: 7.2 - * Version: 18.8.0-rc.1 + * Version: 18.8.0 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/block-supports/background.php b/lib/block-supports/background.php index 57b8d75f03d35..811608127f47e 100644 --- a/lib/block-supports/background.php +++ b/lib/block-supports/background.php @@ -52,12 +52,12 @@ function gutenberg_render_background_support( $block_content, $block ) { return $block_content; } - $background_styles = array(); - $background_styles['backgroundImage'] = $block_attributes['style']['background']['backgroundImage'] ?? null; - $background_styles['backgroundSize'] = $block_attributes['style']['background']['backgroundSize'] ?? null; - $background_styles['backgroundPosition'] = $block_attributes['style']['background']['backgroundPosition'] ?? null; - $background_styles['backgroundRepeat'] = $block_attributes['style']['background']['backgroundRepeat'] ?? null; - + $background_styles = array(); + $background_styles['backgroundImage'] = $block_attributes['style']['background']['backgroundImage'] ?? null; + $background_styles['backgroundSize'] = $block_attributes['style']['background']['backgroundSize'] ?? null; + $background_styles['backgroundPosition'] = $block_attributes['style']['background']['backgroundPosition'] ?? null; + $background_styles['backgroundRepeat'] = $block_attributes['style']['background']['backgroundRepeat'] ?? null; + $background_styles['backgroundAttachment'] = $block_attributes['style']['background']['backgroundAttachment'] ?? null; if ( ! empty( $background_styles['backgroundImage'] ) ) { $background_styles['backgroundSize'] = $background_styles['backgroundSize'] ?? 'cover'; // If the background size is set to `contain` and no position is set, set the position to `center`. diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index ac1f0c57ae9fc..350d158cd6e24 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -306,14 +306,22 @@ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support * They're added separately because padding might only be set on one side. */ if ( isset( $block_spacing_values['declarations']['padding-right'] ) ) { - $padding_right = $block_spacing_values['declarations']['padding-right']; + $padding_right = $block_spacing_values['declarations']['padding-right']; + // Add unit if 0. + if ( '0' === $padding_right ) { + $padding_right = '0px'; + } $layout_styles[] = array( 'selector' => "$selector > .alignfull", 'declarations' => array( 'margin-right' => "calc($padding_right * -1)" ), ); } if ( isset( $block_spacing_values['declarations']['padding-left'] ) ) { - $padding_left = $block_spacing_values['declarations']['padding-left']; + $padding_left = $block_spacing_values['declarations']['padding-left']; + // Add unit if 0. + if ( '0' === $padding_left ) { + $padding_left = '0px'; + } $layout_styles[] = array( 'selector' => "$selector > .alignfull", 'declarations' => array( 'margin-left' => "calc($padding_left * -1)" ), diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 049b1c3e9124a..65a5e5fe4b957 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -234,6 +234,7 @@ class WP_Theme_JSON_Gutenberg { 'background-position' => array( 'background', 'backgroundPosition' ), 'background-repeat' => array( 'background', 'backgroundRepeat' ), 'background-size' => array( 'background', 'backgroundSize' ), + 'background-attachment' => array( 'background', 'backgroundAttachment' ), 'border-radius' => array( 'border', 'radius' ), 'border-top-left-radius' => array( 'border', 'radius', 'topLeft' ), 'border-top-right-radius' => array( 'border', 'radius', 'topRight' ), @@ -503,10 +504,11 @@ class WP_Theme_JSON_Gutenberg { */ const VALID_STYLES = array( 'background' => array( - 'backgroundImage' => null, - 'backgroundPosition' => null, - 'backgroundRepeat' => null, - 'backgroundSize' => null, + 'backgroundImage' => null, + 'backgroundAttachment' => null, + 'backgroundPosition' => null, + 'backgroundRepeat' => null, + 'backgroundSize' => null, ), 'border' => array( 'color' => null, @@ -2919,6 +2921,9 @@ static function ( $pseudo_selector ) use ( $selector ) { } /* + * Root selector (body) styles should not be wrapped in `:root where()` to keep + * specificity at (0,0,1) and maintain backwards compatibility. + * * Top-level element styles using element-only specificity selectors should * not get wrapped in `:root :where()` to maintain backwards compatibility. * @@ -2926,11 +2931,13 @@ static function ( $pseudo_selector ) use ( $selector ) { * still need to be wrapped in `:root :where` to cap specificity for nested * variations etc. Pseudo selectors won't match the ELEMENTS selector exactly. */ - $element_only_selector = $current_element && - isset( static::ELEMENTS[ $current_element ] ) && - // buttons, captions etc. still need `:root :where()` as they are class based selectors. - ! isset( static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $current_element ] ) && - static::ELEMENTS[ $current_element ] === $selector; + $element_only_selector = $is_root_selector || ( + $current_element && + isset( static::ELEMENTS[ $current_element ] ) && + // buttons, captions etc. still need `:root :where()` as they are class based selectors. + ! isset( static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $current_element ] ) && + static::ELEMENTS[ $current_element ] === $selector + ); // 2. Generate and append the rules that use the general selector. $general_selector = $element_only_selector ? $selector : ":root :where($selector)"; diff --git a/lib/compat/wordpress-6.7/class-gutenberg-token-map-6-7.php b/lib/compat/wordpress-6.7/class-gutenberg-token-map-6-7.php new file mode 100644 index 0000000000000..a2142171ddc14 --- /dev/null +++ b/lib/compat/wordpress-6.7/class-gutenberg-token-map-6-7.php @@ -0,0 +1,820 @@ + '😯', + * ':(' => '🙁', + * ':)' => '🙂', + * ':?' => '😕', + * ) ); + * + * true === $smilies->contains( ':)' ); + * false === $smilies->contains( 'simile' ); + * + * '😕' === $smilies->read_token( 'Not sure :?.', 9, $length_of_smily_syntax ); + * 2 === $length_of_smily_syntax; + * + * ## Precomputing the Token Map. + * + * Creating the class involves some work sorting and organizing the tokens and their + * replacement values. In order to skip this, it's possible for the class to export + * its state and be used as actual PHP source code. + * + * Example: + * + * // Export with four spaces as the indent, only for the sake of this docblock. + * // The default indent is a tab character. + * $indent = ' '; + * echo $smilies->precomputed_php_source_table( $indent ); + * + * // Output, to be pasted into a PHP source file: + * WP_Token_Map::from_precomputed_table( + * array( + * "storage_version" => "6.6.0", + * "key_length" => 2, + * "groups" => "", + * "long_words" => array(), + * "small_words" => "8O\x00:)\x00:(\x00:?\x00", + * "small_mappings" => array( "😯", "🙂", "🙁", "😕" ) + * ) + * ); + * + * ## Large vs. small words. + * + * This class uses a short prefix called the "key" to optimize lookup of its tokens. + * This means that some tokens may be shorter than or equal in length to that key. + * Those words that are longer than the key are called "large" while those shorter + * than or equal to the key length are called "small." + * + * This separation of large and small words is incidental to the way this class + * optimizes lookup, and should be considered an internal implementation detail + * of the class. It may still be important to be aware of it, however. + * + * ## Determining Key Length. + * + * The choice of the size of the key length should be based on the data being stored in + * the token map. It should divide the data as evenly as possible, but should not create + * so many groups that a large fraction of the groups only contain a single token. + * + * For the HTML5 named character references, a key length of 2 was found to provide a + * sufficient spread and should be a good default for relatively large sets of tokens. + * + * However, for some data sets this might be too long. For example, a list of smilies + * may be too small for a key length of 2. Perhaps 1 would be more appropriate. It's + * best to experiment and determine empirically which values are appropriate. + * + * ## Generate Pre-Computed Source Code. + * + * Since the `WP_Token_Map` is designed for relatively static lookups, it can be + * advantageous to precompute the values and instantiate a table that has already + * sorted and grouped the tokens and built the lookup strings. + * + * This can be done with `WP_Token_Map::precomputed_php_source_table()`. + * + * Note that if there is a leading character that all tokens need, such as `&` for + * HTML named character references, it can be beneficial to exclude this from the + * token map. Instead, find occurrences of the leading character and then use the + * token map to see if the following characters complete the token. + * + * Example: + * + * $map = WP_Token_Map::from_array( array( 'simple_smile:' => '🙂', 'sob:' => '😭', 'soba:' => '🍜' ) ); + * echo $map->precomputed_php_source_table(); + * // Output + * WP_Token_Map::from_precomputed_table( + * array( + * "storage_version" => "6.6.0", + * "key_length" => 2, + * "groups" => "si\x00so\x00", + * "long_words" => array( + * // simple_smile:[🙂]. + * "\x0bmple_smile:\x04🙂", + * // soba:[🍜] sob:[😭]. + * "\x03ba:\x04🍜\x02b:\x04😭", + * ), + * "short_words" => "", + * "short_mappings" => array() + * } + * ); + * + * This precomputed value can be stored directly in source code and will skip the + * startup cost of generating the lookup strings. See `$html5_named_character_entities`. + * + * Note that any updates to the precomputed format should update the storage version + * constant. It would also be best to provide an update function to take older known + * versions and upgrade them in place when loading into `from_precomputed_table()`. + * + * ## Future Direction. + * + * It may be viable to dynamically increase the length limits such that there's no need to impose them. + * The limit appears because of the packing structure, which indicates how many bytes each segment of + * text in the lookup tables spans. If, however, care were taken to track the longest word length, then + * the packing structure could change its representation to allow for that. Each additional byte storing + * length, however, increases the memory overhead and lookup runtime. + * + * An alternative approach could be to borrow the UTF-8 variable-length encoding and store lengths of less + * than 127 as a single byte with the high bit unset, storing longer lengths as the combination of + * continuation bytes. + * + * Since it has not been shown during the development of this class that longer strings are required, this + * update is deferred until such a need is clear. + * + * @since 6.6.0 + */ +class Gutenberg_Token_Map_6_7 { + /** + * Denotes the version of the code which produces pre-computed source tables. + * + * This version will be used not only to verify pre-computed data, but also + * to upgrade pre-computed data from older versions. Choosing a name that + * corresponds to the WordPress release will help people identify where an + * old copy of data came from. + */ + const STORAGE_VERSION = '6.6.0-trunk'; + + /** + * Maximum length for each key and each transformed value in the table (in bytes). + * + * @since 6.6.0 + */ + const MAX_LENGTH = 256; + + /** + * How many bytes of each key are used to form a group key for lookup. + * This also determines whether a word is considered short or long. + * + * @since 6.6.0 + * + * @var int + */ + private $key_length = 2; + + /** + * Stores an optimized form of the word set, where words are grouped + * by a prefix of the `$key_length` and then collapsed into a string. + * + * In each group, the keys and lookups form a packed data structure. + * The keys in the string are stripped of their "group key," which is + * the prefix of length `$this->key_length` shared by all of the items + * in the group. Each word in the string is prefixed by a single byte + * whose raw unsigned integer value represents how many bytes follow. + * + * ┌────────────────┬───────────────┬─────────────────┬────────┐ + * │ Length of rest │ Rest of key │ Length of value │ Value │ + * │ of key (bytes) │ │ (bytes) │ │ + * ├────────────────┼───────────────┼─────────────────┼────────┤ + * │ 0x08 │ nterDot; │ 0x02 │ · │ + * └────────────────┴───────────────┴─────────────────┴────────┘ + * + * In this example, the key `CenterDot;` has a group key `Ce`, leaving + * eight bytes for the rest of the key, `nterDot;`, and two bytes for + * the transformed value `·` (or U+B7 or "\xC2\xB7"). + * + * Example: + * + * // Stores array( 'CenterDot;' => '·', 'Cedilla;' => '¸' ). + * $groups = "Ce\x00"; + * $large_words = array( "\x08nterDot;\x02·\x06dilla;\x02¸" ) + * + * The prefixes appear in the `$groups` string, each followed by a null + * byte. This makes for quick lookup of where in the group string the key + * is found, and then a simple division converts that offset into the index + * in the `$large_words` array where the group string is to be found. + * + * This lookup data structure is designed to optimize cache locality and + * minimize indirect memory reads when matching strings in the set. + * + * @since 6.6.0 + * + * @var array + */ + private $large_words = array(); + + /** + * Stores the group keys for sequential string lookup. + * + * The offset into this string where the group key appears corresponds with the index + * into the group array where the rest of the group string appears. This is an optimization + * to improve cache locality while searching and minimize indirect memory accesses. + * + * @since 6.6.0 + * + * @var string + */ + private $groups = ''; + + /** + * Stores an optimized row of small words, where every entry is + * `$this->key_size + 1` bytes long and zero-extended. + * + * This packing allows for direct lookup of a short word followed + * by the null byte, if extended to `$this->key_size + 1`. + * + * Example: + * + * // Stores array( 'GT', 'LT', 'gt', 'lt' ). + * "GT\x00LT\x00gt\x00lt\x00" + * + * @since 6.6.0 + * + * @var string + */ + private $small_words = ''; + + /** + * Replacements for the small words, in the same order they appear. + * + * With the position of a small word it's possible to index the translation + * directly, as its position in the `$small_words` string corresponds to + * the index of the replacement in the `$small_mapping` array. + * + * Example: + * + * array( '>', '<', '>', '<' ) + * + * @since 6.6.0 + * + * @var string[] + */ + private $small_mappings = array(); + + /** + * Create a token map using an associative array of key/value pairs as the input. + * + * Example: + * + * $smilies = WP_Token_Map::from_array( array( + * '8O' => '😯', + * ':(' => '🙁', + * ':)' => '🙂', + * ':?' => '😕', + * ) ); + * + * @since 6.6.0 + * + * @param array $mappings The keys transform into the values, both are strings. + * @param int $key_length Determines the group key length. Leave at the default value + * of 2 unless there's an empirical reason to change it. + * + * @return WP_Token_Map|null Token map, unless unable to create it. + */ + public static function from_array( array $mappings, int $key_length = 2 ): ?WP_Token_Map { + $map = new WP_Token_Map(); + $map->key_length = $key_length; + + // Start by grouping words. + + $groups = array(); + $shorts = array(); + foreach ( $mappings as $word => $mapping ) { + if ( + self::MAX_LENGTH <= strlen( $word ) || + self::MAX_LENGTH <= strlen( $mapping ) + ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: 1: maximum byte length (a count) */ + __( 'Token Map tokens and substitutions must all be shorter than %1$d bytes.' ), + self::MAX_LENGTH + ), + '6.6.0' + ); + return null; + } + + $length = strlen( $word ); + + if ( $key_length >= $length ) { + $shorts[] = $word; + } else { + $group = substr( $word, 0, $key_length ); + + if ( ! isset( $groups[ $group ] ) ) { + $groups[ $group ] = array(); + } + + $groups[ $group ][] = array( substr( $word, $key_length ), $mapping ); + } + } + + /* + * Sort the words to ensure that no smaller substring of a match masks the full match. + * For example, `Cap` should not match before `CapitalDifferentialD`. + */ + usort( $shorts, 'WP_Token_Map::longest_first_then_alphabetical' ); + foreach ( $groups as $group_key => $group ) { + usort( + $groups[ $group_key ], + static function ( array $a, array $b ): int { + return self::longest_first_then_alphabetical( $a[0], $b[0] ); + } + ); + } + + // Finally construct the optimized lookups. + + foreach ( $shorts as $word ) { + $map->small_words .= str_pad( $word, $key_length + 1, "\x00", STR_PAD_RIGHT ); + $map->small_mappings[] = $mappings[ $word ]; + } + + $group_keys = array_keys( $groups ); + sort( $group_keys ); + + foreach ( $group_keys as $group ) { + $map->groups .= "{$group}\x00"; + + $group_string = ''; + + foreach ( $groups[ $group ] as $group_word ) { + list( $word, $mapping ) = $group_word; + + $word_length = pack( 'C', strlen( $word ) ); + $mapping_length = pack( 'C', strlen( $mapping ) ); + $group_string .= "{$word_length}{$word}{$mapping_length}{$mapping}"; + } + + $map->large_words[] = $group_string; + } + + return $map; + } + + /** + * Creates a token map from a pre-computed table. + * This skips the initialization cost of generating the table. + * + * This function should only be used to load data created with + * WP_Token_Map::precomputed_php_source_tag(). + * + * @since 6.6.0 + * + * @param array $state { + * Stores pre-computed state for directly loading into a Token Map. + * + * @type string $storage_version Which version of the code produced this state. + * @type int $key_length Group key length. + * @type string $groups Group lookup index. + * @type array $large_words Large word groups and packed strings. + * @type string $small_words Small words packed string. + * @type array $small_mappings Small word mappings. + * } + * + * @return WP_Token_Map Map with precomputed data loaded. + */ + public static function from_precomputed_table( $state ): ?WP_Token_Map { + $has_necessary_state = isset( + $state['storage_version'], + $state['key_length'], + $state['groups'], + $state['large_words'], + $state['small_words'], + $state['small_mappings'] + ); + + if ( ! $has_necessary_state ) { + _doing_it_wrong( + __METHOD__, + __( 'Missing required inputs to pre-computed WP_Token_Map.' ), + '6.6.0' + ); + return null; + } + + if ( self::STORAGE_VERSION !== $state['storage_version'] ) { + _doing_it_wrong( + __METHOD__, + /* translators: 1: version string, 2: version string. */ + sprintf( __( 'Loaded version \'%1$s\' incompatible with expected version \'%2$s\'.' ), $state['storage_version'], self::STORAGE_VERSION ), + '6.6.0' + ); + return null; + } + + $map = new WP_Token_Map(); + + $map->key_length = $state['key_length']; + $map->groups = $state['groups']; + $map->large_words = $state['large_words']; + $map->small_words = $state['small_words']; + $map->small_mappings = $state['small_mappings']; + + return $map; + } + + /** + * Indicates if a given word is a lookup key in the map. + * + * Example: + * + * true === $smilies->contains( ':)' ); + * false === $smilies->contains( 'simile' ); + * + * @since 6.6.0 + * + * @param string $word Determine if this word is a lookup key in the map. + * @param string $case_sensitivity Optional. Pass 'ascii-case-insensitive' to ignore ASCII case when matching. Default 'case-sensitive'. + * @return bool Whether there's an entry for the given word in the map. + */ + public function contains( string $word, string $case_sensitivity = 'case-sensitive' ): bool { + $ignore_case = 'ascii-case-insensitive' === $case_sensitivity; + + if ( $this->key_length >= strlen( $word ) ) { + if ( 0 === strlen( $this->small_words ) ) { + return false; + } + + $term = str_pad( $word, $this->key_length + 1, "\x00", STR_PAD_RIGHT ); + $word_at = $ignore_case ? stripos( $this->small_words, $term ) : strpos( $this->small_words, $term ); + if ( false === $word_at ) { + return false; + } + + return true; + } + + $group_key = substr( $word, 0, $this->key_length ); + $group_at = $ignore_case ? stripos( $this->groups, $group_key ) : strpos( $this->groups, $group_key ); + if ( false === $group_at ) { + return false; + } + $group = $this->large_words[ $group_at / ( $this->key_length + 1 ) ]; + $group_length = strlen( $group ); + $slug = substr( $word, $this->key_length ); + $length = strlen( $slug ); + $at = 0; + + while ( $at < $group_length ) { + $token_length = unpack( 'C', $group[ $at++ ] )[1]; + $token_at = $at; + $at += $token_length; + $mapping_length = unpack( 'C', $group[ $at++ ] )[1]; + $mapping_at = $at; + + if ( $token_length === $length && 0 === substr_compare( $group, $slug, $token_at, $token_length, $ignore_case ) ) { + return true; + } + + $at = $mapping_at + $mapping_length; + } + + return false; + } + + /** + * If the text starting at a given offset is a lookup key in the map, + * return the corresponding transformation from the map, else `false`. + * + * This function returns the translated string, but accepts an optional + * parameter `$matched_token_byte_length`, which communicates how many + * bytes long the lookup key was, if it found one. This can be used to + * advance a cursor in calling code if a lookup key was found. + * + * Example: + * + * false === $smilies->read_token( 'Not sure :?.', 0, $token_byte_length ); + * '😕' === $smilies->read_token( 'Not sure :?.', 9, $token_byte_length ); + * 2 === $token_byte_length; + * + * Example: + * + * while ( $at < strlen( $input ) ) { + * $next_at = strpos( $input, ':', $at ); + * if ( false === $next_at ) { + * break; + * } + * + * $smily = $smilies->read_token( $input, $next_at, $token_byte_length ); + * if ( false === $next_at ) { + * ++$at; + * continue; + * } + * + * $prefix = substr( $input, $at, $next_at - $at ); + * $at += $token_byte_length; + * $output .= "{$prefix}{$smily}"; + * } + * + * @since 6.6.0 + * + * @param string $text String in which to search for a lookup key. + * @param int $offset Optional. How many bytes into the string where the lookup key ought to start. Default 0. + * @param int|null &$matched_token_byte_length Optional. Holds byte-length of found token matched, otherwise not set. Default null. + * @param string $case_sensitivity Optional. Pass 'ascii-case-insensitive' to ignore ASCII case when matching. Default 'case-sensitive'. + * + * @return string|null Mapped value of lookup key if found, otherwise `null`. + */ + public function read_token( string $text, int $offset = 0, &$matched_token_byte_length = null, $case_sensitivity = 'case-sensitive' ): ?string { + $ignore_case = 'ascii-case-insensitive' === $case_sensitivity; + $text_length = strlen( $text ); + + // Search for a long word first, if the text is long enough, and if that fails, a short one. + if ( $text_length > $this->key_length ) { + $group_key = substr( $text, $offset, $this->key_length ); + + $group_at = $ignore_case ? stripos( $this->groups, $group_key ) : strpos( $this->groups, $group_key ); + if ( false === $group_at ) { + // Perhaps a short word then. + return strlen( $this->small_words ) > 0 + ? $this->read_small_token( $text, $offset, $matched_token_byte_length, $case_sensitivity ) + : null; + } + + $group = $this->large_words[ $group_at / ( $this->key_length + 1 ) ]; + $group_length = strlen( $group ); + $at = 0; + while ( $at < $group_length ) { + $token_length = unpack( 'C', $group[ $at++ ] )[1]; + $token = substr( $group, $at, $token_length ); + $at += $token_length; + $mapping_length = unpack( 'C', $group[ $at++ ] )[1]; + $mapping_at = $at; + + if ( 0 === substr_compare( $text, $token, $offset + $this->key_length, $token_length, $ignore_case ) ) { + $matched_token_byte_length = $this->key_length + $token_length; + return substr( $group, $mapping_at, $mapping_length ); + } + + $at = $mapping_at + $mapping_length; + } + } + + // Perhaps a short word then. + return strlen( $this->small_words ) > 0 + ? $this->read_small_token( $text, $offset, $matched_token_byte_length, $case_sensitivity ) + : null; + } + + /** + * Finds a match for a short word at the index. + * + * @since 6.6.0 + * + * @param string $text String in which to search for a lookup key. + * @param int $offset Optional. How many bytes into the string where the lookup key ought to start. Default 0. + * @param int|null &$matched_token_byte_length Optional. Holds byte-length of found lookup key if matched, otherwise not set. Default null. + * @param string $case_sensitivity Optional. Pass 'ascii-case-insensitive' to ignore ASCII case when matching. Default 'case-sensitive'. + * + * @return string|null Mapped value of lookup key if found, otherwise `null`. + */ + private function read_small_token( string $text, int $offset = 0, &$matched_token_byte_length = null, $case_sensitivity = 'case-sensitive' ): ?string { + $ignore_case = 'ascii-case-insensitive' === $case_sensitivity; + $small_length = strlen( $this->small_words ); + $search_text = substr( $text, $offset, $this->key_length ); + if ( $ignore_case ) { + $search_text = strtoupper( $search_text ); + } + $starting_char = $search_text[0]; + + $at = 0; + while ( $at < $small_length ) { + if ( + $starting_char !== $this->small_words[ $at ] && + ( ! $ignore_case || strtoupper( $this->small_words[ $at ] ) !== $starting_char ) + ) { + $at += $this->key_length + 1; + continue; + } + + for ( $adjust = 1; $adjust < $this->key_length; $adjust++ ) { + if ( "\x00" === $this->small_words[ $at + $adjust ] ) { + $matched_token_byte_length = $adjust; + return $this->small_mappings[ $at / ( $this->key_length + 1 ) ]; + } + + if ( + $search_text[ $adjust ] !== $this->small_words[ $at + $adjust ] && + ( ! $ignore_case || strtoupper( $this->small_words[ $at + $adjust ] !== $search_text[ $adjust ] ) ) + ) { + $at += $this->key_length + 1; + continue 2; + } + } + + $matched_token_byte_length = $adjust; + return $this->small_mappings[ $at / ( $this->key_length + 1 ) ]; + } + + return null; + } + + /** + * Exports the token map into an associate array of key/value pairs. + * + * Example: + * + * $smilies->to_array() === array( + * '8O' => '😯', + * ':(' => '🙁', + * ':)' => '🙂', + * ':?' => '😕', + * ); + * + * @return array The lookup key/substitution values as an associate array. + */ + public function to_array(): array { + $tokens = array(); + + $at = 0; + $small_mapping = 0; + $small_length = strlen( $this->small_words ); + while ( $at < $small_length ) { + $key = rtrim( substr( $this->small_words, $at, $this->key_length + 1 ), "\x00" ); + $value = $this->small_mappings[ $small_mapping++ ]; + $tokens[ $key ] = $value; + + $at += $this->key_length + 1; + } + + foreach ( $this->large_words as $index => $group ) { + $prefix = substr( $this->groups, $index * ( $this->key_length + 1 ), 2 ); + $group_length = strlen( $group ); + $at = 0; + while ( $at < $group_length ) { + $length = unpack( 'C', $group[ $at++ ] )[1]; + $key = $prefix . substr( $group, $at, $length ); + + $at += $length; + $length = unpack( 'C', $group[ $at++ ] )[1]; + $value = substr( $group, $at, $length ); + + $tokens[ $key ] = $value; + $at += $length; + } + } + + return $tokens; + } + + /** + * Export the token map for quick loading in PHP source code. + * + * This function has a specific purpose, to make loading of static token maps fast. + * It's used to ensure that the HTML character reference lookups add a minimal cost + * to initializing the PHP process. + * + * Example: + * + * echo $smilies->precomputed_php_source_table(); + * + * // Output. + * WP_Token_Map::from_precomputed_table( + * array( + * "storage_version" => "6.6.0", + * "key_length" => 2, + * "groups" => "", + * "long_words" => array(), + * "small_words" => "8O\x00:)\x00:(\x00:?\x00", + * "small_mappings" => array( "😯", "🙂", "🙁", "😕" ) + * ) + * ); + * + * @since 6.6.0 + * + * @param string $indent Optional. Use this string for indentation, or rely on the default horizontal tab character. Default "\t". + * @return string Value which can be pasted into a PHP source file for quick loading of table. + */ + public function precomputed_php_source_table( string $indent = "\t" ): string { + $i1 = $indent; + $i2 = $i1 . $indent; + $i3 = $i2 . $indent; + + $class_version = self::STORAGE_VERSION; + + $output = self::class . "::from_precomputed_table(\n"; + $output .= "{$i1}array(\n"; + $output .= "{$i2}\"storage_version\" => \"{$class_version}\",\n"; + $output .= "{$i2}\"key_length\" => {$this->key_length},\n"; + + $group_line = str_replace( "\x00", "\\x00", $this->groups ); + $output .= "{$i2}\"groups\" => \"{$group_line}\",\n"; + + $output .= "{$i2}\"large_words\" => array(\n"; + + $prefixes = explode( "\x00", $this->groups ); + foreach ( $prefixes as $index => $prefix ) { + if ( '' === $prefix ) { + break; + } + $group = $this->large_words[ $index ]; + $group_length = strlen( $group ); + $comment_line = "{$i3}//"; + $data_line = "{$i3}\""; + $at = 0; + while ( $at < $group_length ) { + $token_length = unpack( 'C', $group[ $at++ ] )[1]; + $token = substr( $group, $at, $token_length ); + $at += $token_length; + $mapping_length = unpack( 'C', $group[ $at++ ] )[1]; + $mapping = substr( $group, $at, $mapping_length ); + $at += $mapping_length; + + $token_digits = str_pad( dechex( $token_length ), 2, '0', STR_PAD_LEFT ); + $mapping_digits = str_pad( dechex( $mapping_length ), 2, '0', STR_PAD_LEFT ); + + $mapping = preg_replace_callback( + "~[\\x00-\\x1f\\x22\\x5c]~", + static function ( $match_result ) { + switch ( $match_result[0] ) { + case '"': + return '\\"'; + + case '\\': + return '\\\\'; + + default: + $hex = dechex( ord( $match_result[0] ) ); + return "\\x{$hex}"; + } + }, + $mapping + ); + + $comment_line .= " {$prefix}{$token}[{$mapping}]"; + $data_line .= "\\x{$token_digits}{$token}\\x{$mapping_digits}{$mapping}"; + } + $comment_line .= ".\n"; + $data_line .= "\",\n"; + + $output .= $comment_line; + $output .= $data_line; + } + + $output .= "{$i2}),\n"; + + $small_words = array(); + $small_length = strlen( $this->small_words ); + $at = 0; + while ( $at < $small_length ) { + $small_words[] = substr( $this->small_words, $at, $this->key_length + 1 ); + $at += $this->key_length + 1; + } + + $small_text = str_replace( "\x00", '\x00', implode( '', $small_words ) ); + $output .= "{$i2}\"small_words\" => \"{$small_text}\",\n"; + + $output .= "{$i2}\"small_mappings\" => array(\n"; + foreach ( $this->small_mappings as $mapping ) { + $output .= "{$i3}\"{$mapping}\",\n"; + } + $output .= "{$i2})\n"; + $output .= "{$i1})\n"; + $output .= ')'; + + return $output; + } + + /** + * Compares two strings, returning the longest, or whichever + * is first alphabetically if they are the same length. + * + * This is an important sort when building the token map because + * it should not form a match on a substring of a longer potential + * match. For example, it should not detect `Cap` when matching + * against the string `CapitalDifferentialD`. + * + * @since 6.6.0 + * + * @param string $a First string to compare. + * @param string $b Second string to compare. + * @return int -1 or lower if `$a` is less than `$b`; 1 or greater if `$a` is greater than `$b`, and 0 if they are equal. + */ + private static function longest_first_then_alphabetical( string $a, string $b ): int { + if ( $a === $b ) { + return 0; + } + + $length_a = strlen( $a ); + $length_b = strlen( $b ); + + // Longer strings are less-than for comparison's sake. + if ( $length_a !== $length_b ) { + return $length_b - $length_a; + } + + return strcmp( $a, $b ); + } +} diff --git a/lib/compat/wordpress-6.7/html-api/class-gutenberg-html-active-formatting-elements-6-7.php b/lib/compat/wordpress-6.7/html-api/class-gutenberg-html-active-formatting-elements-6-7.php new file mode 100644 index 0000000000000..10f53fe82ce4e --- /dev/null +++ b/lib/compat/wordpress-6.7/html-api/class-gutenberg-html-active-formatting-elements-6-7.php @@ -0,0 +1,187 @@ + Initially, the list of active formatting elements is empty. + * > It is used to handle mis-nested formatting element tags. + * > + * > The list contains elements in the formatting category, and markers. + * > The markers are inserted when entering applet, object, marquee, + * > template, td, th, and caption elements, and are used to prevent + * > formatting from "leaking" into applet, object, marquee, template, + * > td, th, and caption elements. + * > + * > In addition, each element in the list of active formatting elements + * > is associated with the token for which it was created, so that + * > further elements can be created for that token if necessary. + * + * @since 6.4.0 + * + * @access private + * + * @see https://html.spec.whatwg.org/#list-of-active-formatting-elements + * @see WP_HTML_Processor + */ +class Gutenberg_HTML_Active_Formatting_Elements_6_7 { + /** + * Holds the stack of active formatting element references. + * + * @since 6.4.0 + * + * @var WP_HTML_Token[] + */ + private $stack = array(); + + /** + * Reports if a specific node is in the stack of active formatting elements. + * + * @since 6.4.0 + * + * @param WP_HTML_Token $token Look for this node in the stack. + * @return bool Whether the referenced node is in the stack of active formatting elements. + */ + public function contains_node( Gutenberg_HTML_Token_6_7 $token ) { + foreach ( $this->walk_up() as $item ) { + if ( $token->bookmark_name === $item->bookmark_name ) { + return true; + } + } + + return false; + } + + /** + * Returns how many nodes are currently in the stack of active formatting elements. + * + * @since 6.4.0 + * + * @return int How many node are in the stack of active formatting elements. + */ + public function count() { + return count( $this->stack ); + } + + /** + * Returns the node at the end of the stack of active formatting elements, + * if one exists. If the stack is empty, returns null. + * + * @since 6.4.0 + * + * @return WP_HTML_Token|null Last node in the stack of active formatting elements, if one exists, otherwise null. + */ + public function current_node() { + $current_node = end( $this->stack ); + + return $current_node ? $current_node : null; + } + + /** + * Pushes a node onto the stack of active formatting elements. + * + * @since 6.4.0 + * + * @see https://html.spec.whatwg.org/#push-onto-the-list-of-active-formatting-elements + * + * @param WP_HTML_Token $token Push this node onto the stack. + */ + public function push( Gutenberg_HTML_Token_6_7 $token ) { + /* + * > If there are already three elements in the list of active formatting elements after the last marker, + * > if any, or anywhere in the list if there are no markers, that have the same tag name, namespace, and + * > attributes as element, then remove the earliest such element from the list of active formatting + * > elements. For these purposes, the attributes must be compared as they were when the elements were + * > created by the parser; two elements have the same attributes if all their parsed attributes can be + * > paired such that the two attributes in each pair have identical names, namespaces, and values + * > (the order of the attributes does not matter). + * + * @todo Implement the "Noah's Ark clause" to only add up to three of any given kind of formatting elements to the stack. + */ + // > Add element to the list of active formatting elements. + $this->stack[] = $token; + } + + /** + * Removes a node from the stack of active formatting elements. + * + * @since 6.4.0 + * + * @param WP_HTML_Token $token Remove this node from the stack, if it's there already. + * @return bool Whether the node was found and removed from the stack of active formatting elements. + */ + public function remove_node( Gutenberg_HTML_Token_6_7 $token ) { + foreach ( $this->walk_up() as $position_from_end => $item ) { + if ( $token->bookmark_name !== $item->bookmark_name ) { + continue; + } + + $position_from_start = $this->count() - $position_from_end - 1; + array_splice( $this->stack, $position_from_start, 1 ); + return true; + } + + return false; + } + + /** + * Steps through the stack of active formatting elements, starting with the + * top element (added first) and walking downwards to the one added last. + * + * This generator function is designed to be used inside a "foreach" loop. + * + * Example: + * + * $html = 'We are here'; + * foreach ( $stack->walk_down() as $node ) { + * echo "{$node->node_name} -> "; + * } + * > EM -> STRONG -> A -> + * + * To start with the most-recently added element and walk towards the top, + * see WP_HTML_Active_Formatting_Elements::walk_up(). + * + * @since 6.4.0 + */ + public function walk_down() { + $count = count( $this->stack ); + + for ( $i = 0; $i < $count; $i++ ) { + yield $this->stack[ $i ]; + } + } + + /** + * Steps through the stack of active formatting elements, starting with the + * bottom element (added last) and walking upwards to the one added first. + * + * This generator function is designed to be used inside a "foreach" loop. + * + * Example: + * + * $html = 'We are here'; + * foreach ( $stack->walk_up() as $node ) { + * echo "{$node->node_name} -> "; + * } + * > A -> STRONG -> EM -> + * + * To start with the first added element and walk towards the bottom, + * see WP_HTML_Active_Formatting_Elements::walk_down(). + * + * @since 6.4.0 + */ + public function walk_up() { + for ( $i = count( $this->stack ) - 1; $i >= 0; $i-- ) { + yield $this->stack[ $i ]; + } + } +} diff --git a/lib/compat/wordpress-6.7/html-api/class-gutenberg-html-attribute-token-6-7.php b/lib/compat/wordpress-6.7/html-api/class-gutenberg-html-attribute-token-6-7.php new file mode 100644 index 0000000000000..4ee369b795c84 --- /dev/null +++ b/lib/compat/wordpress-6.7/html-api/class-gutenberg-html-attribute-token-6-7.php @@ -0,0 +1,116 @@ + + * ------------ length is 12, including quotes + * + * + * ------- length is 6 + * + * + * ------------ length is 11 + * + * @since 6.5.0 Replaced `end` with `length` to more closely match `substr()`. + * + * @var int + */ + public $length; + + /** + * Whether the attribute is a boolean attribute with value `true`. + * + * @since 6.2.0 + * + * @var bool + */ + public $is_true; + + /** + * Constructor. + * + * @since 6.2.0 + * @since 6.5.0 Replaced `end` with `length` to more closely match `substr()`. + * + * @param string $name Attribute name. + * @param int $value_start Attribute value. + * @param int $value_length Number of bytes attribute value spans. + * @param int $start The string offset where the attribute name starts. + * @param int $length Byte length of the entire attribute name or name and value pair expression. + * @param bool $is_true Whether the attribute is a boolean attribute with true value. + */ + public function __construct( $name, $value_start, $value_length, $start, $length, $is_true ) { + $this->name = $name; + $this->value_starts_at = $value_start; + $this->value_length = $value_length; + $this->start = $start; + $this->length = $length; + $this->is_true = $is_true; + } +} diff --git a/lib/compat/wordpress-6.7/html-api/class-gutenberg-html-decoder-6-7.php b/lib/compat/wordpress-6.7/html-api/class-gutenberg-html-decoder-6-7.php new file mode 100644 index 0000000000000..70f51151d8647 --- /dev/null +++ b/lib/compat/wordpress-6.7/html-api/class-gutenberg-html-decoder-6-7.php @@ -0,0 +1,461 @@ += $length ) { + return null; + } + + if ( '&' !== $text[ $at ] ) { + return null; + } + + /* + * Numeric character references. + * + * When truncated, these will encode the code point found by parsing the + * digits that are available. For example, when `🅰` is truncated + * to `DZ` it will encode `DZ`. It does not: + * - know how to parse the original `🅰`. + * - fail to parse and return plaintext `DZ`. + * - fail to parse and return the replacement character `�` + */ + if ( '#' === $text[ $at + 1 ] ) { + if ( $at + 2 >= $length ) { + return null; + } + + /** Tracks inner parsing within the numeric character reference. */ + $digits_at = $at + 2; + + if ( 'x' === $text[ $digits_at ] || 'X' === $text[ $digits_at ] ) { + $numeric_base = 16; + $numeric_digits = '0123456789abcdefABCDEF'; + $max_digits = 6; // 􏿿 + ++$digits_at; + } else { + $numeric_base = 10; + $numeric_digits = '0123456789'; + $max_digits = 7; // 􏿿 + } + + // Cannot encode invalid Unicode code points. Max is to U+10FFFF. + $zero_count = strspn( $text, '0', $digits_at ); + $digit_count = strspn( $text, $numeric_digits, $digits_at + $zero_count ); + $after_digits = $digits_at + $zero_count + $digit_count; + $has_semicolon = $after_digits < $length && ';' === $text[ $after_digits ]; + $end_of_span = $has_semicolon ? $after_digits + 1 : $after_digits; + + // `&#` or `&#x` without digits returns into plaintext. + if ( 0 === $digit_count && 0 === $zero_count ) { + return null; + } + + // Whereas `&#` and only zeros is invalid. + if ( 0 === $digit_count ) { + $match_byte_length = $end_of_span - $at; + return '�'; + } + + // If there are too many digits then it's not worth parsing. It's invalid. + if ( $digit_count > $max_digits ) { + $match_byte_length = $end_of_span - $at; + return '�'; + } + + $digits = substr( $text, $digits_at + $zero_count, $digit_count ); + $code_point = intval( $digits, $numeric_base ); + + /* + * Noncharacters, 0x0D, and non-ASCII-whitespace control characters. + * + * > A noncharacter is a code point that is in the range U+FDD0 to U+FDEF, + * > inclusive, or U+FFFE, U+FFFF, U+1FFFE, U+1FFFF, U+2FFFE, U+2FFFF, + * > U+3FFFE, U+3FFFF, U+4FFFE, U+4FFFF, U+5FFFE, U+5FFFF, U+6FFFE, + * > U+6FFFF, U+7FFFE, U+7FFFF, U+8FFFE, U+8FFFF, U+9FFFE, U+9FFFF, + * > U+AFFFE, U+AFFFF, U+BFFFE, U+BFFFF, U+CFFFE, U+CFFFF, U+DFFFE, + * > U+DFFFF, U+EFFFE, U+EFFFF, U+FFFFE, U+FFFFF, U+10FFFE, or U+10FFFF. + * + * A C0 control is a code point that is in the range of U+00 to U+1F, + * but ASCII whitespace includes U+09, U+0A, U+0C, and U+0D. + * + * These characters are invalid but still decode as any valid character. + * This comment is here to note and explain why there's no check to + * remove these characters or replace them. + * + * @see https://infra.spec.whatwg.org/#noncharacter + */ + + /* + * Code points in the C1 controls area need to be remapped as if they + * were stored in Windows-1252. Note! This transformation only happens + * for numeric character references. The raw code points in the byte + * stream are not translated. + * + * > If the number is one of the numbers in the first column of + * > the following table, then find the row with that number in + * > the first column, and set the character reference code to + * > the number in the second column of that row. + */ + if ( $code_point >= 0x80 && $code_point <= 0x9F ) { + $windows_1252_mapping = array( + 0x20AC, // 0x80 -> EURO SIGN (€). + 0x81, // 0x81 -> (no change). + 0x201A, // 0x82 -> SINGLE LOW-9 QUOTATION MARK (‚). + 0x0192, // 0x83 -> LATIN SMALL LETTER F WITH HOOK (ƒ). + 0x201E, // 0x84 -> DOUBLE LOW-9 QUOTATION MARK („). + 0x2026, // 0x85 -> HORIZONTAL ELLIPSIS (…). + 0x2020, // 0x86 -> DAGGER (†). + 0x2021, // 0x87 -> DOUBLE DAGGER (‡). + 0x02C6, // 0x88 -> MODIFIER LETTER CIRCUMFLEX ACCENT (ˆ). + 0x2030, // 0x89 -> PER MILLE SIGN (‰). + 0x0160, // 0x8A -> LATIN CAPITAL LETTER S WITH CARON (Š). + 0x2039, // 0x8B -> SINGLE LEFT-POINTING ANGLE QUOTATION MARK (‹). + 0x0152, // 0x8C -> LATIN CAPITAL LIGATURE OE (Œ). + 0x8D, // 0x8D -> (no change). + 0x017D, // 0x8E -> LATIN CAPITAL LETTER Z WITH CARON (Ž). + 0x8F, // 0x8F -> (no change). + 0x90, // 0x90 -> (no change). + 0x2018, // 0x91 -> LEFT SINGLE QUOTATION MARK (‘). + 0x2019, // 0x92 -> RIGHT SINGLE QUOTATION MARK (’). + 0x201C, // 0x93 -> LEFT DOUBLE QUOTATION MARK (“). + 0x201D, // 0x94 -> RIGHT DOUBLE QUOTATION MARK (”). + 0x2022, // 0x95 -> BULLET (•). + 0x2013, // 0x96 -> EN DASH (–). + 0x2014, // 0x97 -> EM DASH (—). + 0x02DC, // 0x98 -> SMALL TILDE (˜). + 0x2122, // 0x99 -> TRADE MARK SIGN (™). + 0x0161, // 0x9A -> LATIN SMALL LETTER S WITH CARON (š). + 0x203A, // 0x9B -> SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (›). + 0x0153, // 0x9C -> LATIN SMALL LIGATURE OE (œ). + 0x9D, // 0x9D -> (no change). + 0x017E, // 0x9E -> LATIN SMALL LETTER Z WITH CARON (ž). + 0x0178, // 0x9F -> LATIN CAPITAL LETTER Y WITH DIAERESIS (Ÿ). + ); + + $code_point = $windows_1252_mapping[ $code_point - 0x80 ]; + } + + $match_byte_length = $end_of_span - $at; + return self::code_point_to_utf8_bytes( $code_point ); + } + + /** Tracks inner parsing within the named character reference. */ + $name_at = $at + 1; + // Minimum named character reference is two characters. E.g. `GT`. + if ( $name_at + 2 > $length ) { + return null; + } + + $name_length = 0; + $replacement = $html5_named_character_references->read_token( $text, $name_at, $name_length ); + if ( false === $replacement ) { + return null; + } + + $after_name = $name_at + $name_length; + + // If the match ended with a semicolon then it should always be decoded. + if ( ';' === $text[ $name_at + $name_length - 1 ] ) { + $match_byte_length = $after_name - $at; + return $replacement; + } + + /* + * At this point though there's a match for an entry in the named + * character reference table but the match doesn't end in `;`. + * It may be allowed if it's followed by something unambiguous. + */ + $ambiguous_follower = ( + $after_name < $length && + $name_at < $length && + ( + ctype_alnum( $text[ $after_name ] ) || + '=' === $text[ $after_name ] + ) + ); + + // It's non-ambiguous, safe to leave it in. + if ( ! $ambiguous_follower ) { + $match_byte_length = $after_name - $at; + return $replacement; + } + + // It's ambiguous, which isn't allowed inside attributes. + if ( 'attribute' === $context ) { + return null; + } + + $match_byte_length = $after_name - $at; + return $replacement; + } + + /** + * Encode a code point number into the UTF-8 encoding. + * + * This encoder implements the UTF-8 encoding algorithm for converting + * a code point into a byte sequence. If it receives an invalid code + * point it will return the Unicode Replacement Character U+FFFD `�`. + * + * Example: + * + * '🅰' === WP_HTML_Decoder::code_point_to_utf8_bytes( 0x1f170 ); + * + * // Half of a surrogate pair is an invalid code point. + * '�' === WP_HTML_Decoder::code_point_to_utf8_bytes( 0xd83c ); + * + * @since 6.6.0 + * + * @see https://www.rfc-editor.org/rfc/rfc3629 For the UTF-8 standard. + * + * @param int $code_point Which code point to convert. + * @return string Converted code point, or `�` if invalid. + */ + public static function code_point_to_utf8_bytes( $code_point ): string { + // Pre-check to ensure a valid code point. + if ( + $code_point <= 0 || + ( $code_point >= 0xD800 && $code_point <= 0xDFFF ) || + $code_point > 0x10FFFF + ) { + return '�'; + } + + if ( $code_point <= 0x7F ) { + return chr( $code_point ); + } + + if ( $code_point <= 0x7FF ) { + $byte1 = chr( ( $code_point >> 6 ) | 0xC0 ); + $byte2 = chr( $code_point & 0x3F | 0x80 ); + + return "{$byte1}{$byte2}"; + } + + if ( $code_point <= 0xFFFF ) { + $byte1 = chr( ( $code_point >> 12 ) | 0xE0 ); + $byte2 = chr( ( $code_point >> 6 ) & 0x3F | 0x80 ); + $byte3 = chr( $code_point & 0x3F | 0x80 ); + + return "{$byte1}{$byte2}{$byte3}"; + } + + // Any values above U+10FFFF are eliminated above in the pre-check. + $byte1 = chr( ( $code_point >> 18 ) | 0xF0 ); + $byte2 = chr( ( $code_point >> 12 ) & 0x3F | 0x80 ); + $byte3 = chr( ( $code_point >> 6 ) & 0x3F | 0x80 ); + $byte4 = chr( $code_point & 0x3F | 0x80 ); + + return "{$byte1}{$byte2}{$byte3}{$byte4}"; + } +} diff --git a/lib/compat/wordpress-6.7/html-api/class-gutenberg-html-open-elements-6-7.php b/lib/compat/wordpress-6.7/html-api/class-gutenberg-html-open-elements-6-7.php new file mode 100644 index 0000000000000..fd2b252432455 --- /dev/null +++ b/lib/compat/wordpress-6.7/html-api/class-gutenberg-html-open-elements-6-7.php @@ -0,0 +1,598 @@ + Initially, the stack of open elements is empty. The stack grows + * > downwards; the topmost node on the stack is the first one added + * > to the stack, and the bottommost node of the stack is the most + * > recently added node in the stack (notwithstanding when the stack + * > is manipulated in a random access fashion as part of the handling + * > for misnested tags). + * + * @since 6.4.0 + * + * @access private + * + * @see https://html.spec.whatwg.org/#stack-of-open-elements + * @see WP_HTML_Processor + */ +class Gutenberg_HTML_Open_Elements_6_7 { + /** + * Holds the stack of open element references. + * + * @since 6.4.0 + * + * @var WP_HTML_Token[] + */ + public $stack = array(); + + /** + * Whether a P element is in button scope currently. + * + * This class optimizes scope lookup by pre-calculating + * this value when elements are added and removed to the + * stack of open elements which might change its value. + * This avoids frequent iteration over the stack. + * + * @since 6.4.0 + * + * @var bool + */ + private $has_p_in_button_scope = false; + + /** + * A function that will be called when an item is popped off the stack of open elements. + * + * The function will be called with the popped item as its argument. + * + * @since 6.6.0 + * + * @var Closure|null + */ + private $pop_handler = null; + + /** + * A function that will be called when an item is pushed onto the stack of open elements. + * + * The function will be called with the pushed item as its argument. + * + * @since 6.6.0 + * + * @var Closure|null + */ + private $push_handler = null; + + /** + * Sets a pop handler that will be called when an item is popped off the stack of + * open elements. + * + * The function will be called with the pushed item as its argument. + * + * @since 6.6.0 + * + * @param Closure $handler The handler function. + */ + public function set_pop_handler( Closure $handler ): void { + $this->pop_handler = $handler; + } + + /** + * Sets a push handler that will be called when an item is pushed onto the stack of + * open elements. + * + * The function will be called with the pushed item as its argument. + * + * @since 6.6.0 + * + * @param Closure $handler The handler function. + */ + public function set_push_handler( Closure $handler ): void { + $this->push_handler = $handler; + } + + /** + * Reports if a specific node is in the stack of open elements. + * + * @since 6.4.0 + * + * @param WP_HTML_Token $token Look for this node in the stack. + * @return bool Whether the referenced node is in the stack of open elements. + */ + public function contains_node( Gutenberg_HTML_Token_6_7 $token ): bool { + foreach ( $this->walk_up() as $item ) { + if ( $token->bookmark_name === $item->bookmark_name ) { + return true; + } + } + + return false; + } + + /** + * Returns how many nodes are currently in the stack of open elements. + * + * @since 6.4.0 + * + * @return int How many node are in the stack of open elements. + */ + public function count(): int { + return count( $this->stack ); + } + + /** + * Returns the node at the end of the stack of open elements, + * if one exists. If the stack is empty, returns null. + * + * @since 6.4.0 + * + * @return WP_HTML_Token|null Last node in the stack of open elements, if one exists, otherwise null. + */ + public function current_node(): ?Gutenberg_HTML_Token_6_7 { + $current_node = end( $this->stack ); + + return $current_node ? $current_node : null; + } + + /** + * Indicates if the current node is of a given type or name. + * + * It's possible to pass either a node type or a node name to this function. + * In the case there is no current element it will always return `false`. + * + * Example: + * + * // Is the current node a text node? + * $stack->current_node_is( '#text' ); + * + * // Is the current node a DIV element? + * $stack->current_node_is( 'DIV' ); + * + * // Is the current node any element/tag? + * $stack->current_node_is( '#tag' ); + * + * @see WP_HTML_Tag_Processor::get_token_type + * @see WP_HTML_Tag_Processor::get_token_name + * + * @since 6.7.0 + * + * @access private + * + * @param string $identity Check if the current node has this name or type (depending on what is provided). + * @return bool Whether there is a current element that matches the given identity, whether a token name or type. + */ + public function current_node_is( string $identity ): bool { + $current_node = end( $this->stack ); + if ( false === $current_node ) { + return false; + } + + $current_node_name = $current_node->node_name; + + return ( + $current_node_name === $identity || + ( '#doctype' === $identity && 'html' === $current_node_name ) || + ( '#tag' === $identity && ctype_upper( $current_node_name ) ) + ); + } + + /** + * Returns whether an element is in a specific scope. + * + * @since 6.4.0 + * + * @see https://html.spec.whatwg.org/#has-an-element-in-the-specific-scope + * + * @param string $tag_name Name of tag check. + * @param string[] $termination_list List of elements that terminate the search. + * @return bool Whether the element was found in a specific scope. + */ + public function has_element_in_specific_scope( string $tag_name, $termination_list ): bool { + foreach ( $this->walk_up() as $node ) { + if ( $node->node_name === $tag_name ) { + return true; + } + + if ( + '(internal: H1 through H6 - do not use)' === $tag_name && + in_array( $node->node_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) + ) { + return true; + } + + switch ( $node->node_name ) { + case 'HTML': + return false; + } + + if ( in_array( $node->node_name, $termination_list, true ) ) { + return false; + } + } + + return false; + } + + /** + * Returns whether a particular element is in scope. + * + * @since 6.4.0 + * + * @see https://html.spec.whatwg.org/#has-an-element-in-scope + * + * @param string $tag_name Name of tag to check. + * @return bool Whether given element is in scope. + */ + public function has_element_in_scope( string $tag_name ): bool { + return $this->has_element_in_specific_scope( + $tag_name, + array( + + /* + * Because it's not currently possible to encounter + * one of the termination elements, they don't need + * to be listed here. If they were, they would be + * unreachable and only waste CPU cycles while + * scanning through HTML. + */ + ) + ); + } + + /** + * Returns whether a particular element is in list item scope. + * + * @since 6.4.0 + * @since 6.5.0 Implemented: no longer throws on every invocation. + * + * @see https://html.spec.whatwg.org/#has-an-element-in-list-item-scope + * + * @param string $tag_name Name of tag to check. + * @return bool Whether given element is in scope. + */ + public function has_element_in_list_item_scope( string $tag_name ): bool { + return $this->has_element_in_specific_scope( + $tag_name, + array( + // There are more elements that belong here which aren't currently supported. + 'OL', + 'UL', + ) + ); + } + + /** + * Returns whether a particular element is in button scope. + * + * @since 6.4.0 + * + * @see https://html.spec.whatwg.org/#has-an-element-in-button-scope + * + * @param string $tag_name Name of tag to check. + * @return bool Whether given element is in scope. + */ + public function has_element_in_button_scope( string $tag_name ): bool { + return $this->has_element_in_specific_scope( $tag_name, array( 'BUTTON' ) ); + } + + /** + * Returns whether a particular element is in table scope. + * + * @since 6.4.0 + * + * @see https://html.spec.whatwg.org/#has-an-element-in-table-scope + * + * @throws WP_HTML_Unsupported_Exception Always until this function is implemented. + * + * @param string $tag_name Name of tag to check. + * @return bool Whether given element is in scope. + */ + public function has_element_in_table_scope( string $tag_name ): bool { + throw new Gutenberg_HTML_Unsupported_Exception_6_7( 'Cannot process elements depending on table scope.' ); + + return false; // The linter requires this unreachable code until the function is implemented and can return. + } + + /** + * Returns whether a particular element is in select scope. + * + * This test differs from the others like it, in that its rules are inverted. + * Instead of arriving at a match when one of any tag in a termination group + * is reached, this one terminates if any other tag is reached. + * + * > The stack of open elements is said to have a particular element in select scope when it has + * > that element in the specific scope consisting of all element types except the following: + * > - optgroup in the HTML namespace + * > - option in the HTML namespace + * + * @since 6.4.0 Stub implementation (throws). + * @since 6.7.0 Full implementation. + * + * @see https://html.spec.whatwg.org/#has-an-element-in-select-scope + * + * @param string $tag_name Name of tag to check. + * @return bool Whether the given element is in SELECT scope. + */ + public function has_element_in_select_scope( string $tag_name ): bool { + foreach ( $this->walk_up() as $node ) { + if ( $node->node_name === $tag_name ) { + return true; + } + + if ( + 'OPTION' !== $node->node_name && + 'OPTGROUP' !== $node->node_name + ) { + return false; + } + } + + return false; + } + + /** + * Returns whether a P is in BUTTON scope. + * + * @since 6.4.0 + * + * @see https://html.spec.whatwg.org/#has-an-element-in-button-scope + * + * @return bool Whether a P is in BUTTON scope. + */ + public function has_p_in_button_scope(): bool { + return $this->has_p_in_button_scope; + } + + /** + * Pops a node off of the stack of open elements. + * + * @since 6.4.0 + * + * @see https://html.spec.whatwg.org/#stack-of-open-elements + * + * @return bool Whether a node was popped off of the stack. + */ + public function pop(): bool { + $item = array_pop( $this->stack ); + if ( null === $item ) { + return false; + } + + if ( 'context-node' === $item->bookmark_name ) { + $this->stack[] = $item; + return false; + } + + $this->after_element_pop( $item ); + return true; + } + + /** + * Pops nodes off of the stack of open elements until one with the given tag name has been popped. + * + * @since 6.4.0 + * + * @see WP_HTML_Open_Elements::pop + * + * @param string $tag_name Name of tag that needs to be popped off of the stack of open elements. + * @return bool Whether a tag of the given name was found and popped off of the stack of open elements. + */ + public function pop_until( string $tag_name ): bool { + foreach ( $this->walk_up() as $item ) { + if ( 'context-node' === $item->bookmark_name ) { + return true; + } + + $this->pop(); + + if ( + '(internal: H1 through H6 - do not use)' === $tag_name && + in_array( $item->node_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true ) + ) { + return true; + } + + if ( $tag_name === $item->node_name ) { + return true; + } + } + + return false; + } + + /** + * Pushes a node onto the stack of open elements. + * + * @since 6.4.0 + * + * @see https://html.spec.whatwg.org/#stack-of-open-elements + * + * @param WP_HTML_Token $stack_item Item to add onto stack. + */ + public function push( Gutenberg_HTML_Token_6_7 $stack_item ): void { + $this->stack[] = $stack_item; + $this->after_element_push( $stack_item ); + } + + /** + * Removes a specific node from the stack of open elements. + * + * @since 6.4.0 + * + * @param WP_HTML_Token $token The node to remove from the stack of open elements. + * @return bool Whether the node was found and removed from the stack of open elements. + */ + public function remove_node( Gutenberg_HTML_Token_6_7 $token ): bool { + if ( 'context-node' === $token->bookmark_name ) { + return false; + } + + foreach ( $this->walk_up() as $position_from_end => $item ) { + if ( $token->bookmark_name !== $item->bookmark_name ) { + continue; + } + + $position_from_start = $this->count() - $position_from_end - 1; + array_splice( $this->stack, $position_from_start, 1 ); + $this->after_element_pop( $item ); + return true; + } + + return false; + } + + + /** + * Steps through the stack of open elements, starting with the top element + * (added first) and walking downwards to the one added last. + * + * This generator function is designed to be used inside a "foreach" loop. + * + * Example: + * + * $html = 'We are here'; + * foreach ( $stack->walk_down() as $node ) { + * echo "{$node->node_name} -> "; + * } + * > EM -> STRONG -> A -> + * + * To start with the most-recently added element and walk towards the top, + * see WP_HTML_Open_Elements::walk_up(). + * + * @since 6.4.0 + */ + public function walk_down() { + $count = count( $this->stack ); + + for ( $i = 0; $i < $count; $i++ ) { + yield $this->stack[ $i ]; + } + } + + /** + * Steps through the stack of open elements, starting with the bottom element + * (added last) and walking upwards to the one added first. + * + * This generator function is designed to be used inside a "foreach" loop. + * + * Example: + * + * $html = 'We are here'; + * foreach ( $stack->walk_up() as $node ) { + * echo "{$node->node_name} -> "; + * } + * > A -> STRONG -> EM -> + * + * To start with the first added element and walk towards the bottom, + * see WP_HTML_Open_Elements::walk_down(). + * + * @since 6.4.0 + * @since 6.5.0 Accepts $above_this_node to start traversal above a given node, if it exists. + * + * @param WP_HTML_Token|null $above_this_node Optional. Start traversing above this node, + * if provided and if the node exists. + */ + public function walk_up( ?Gutenberg_HTML_Token_6_7 $above_this_node = null ) { + $has_found_node = null === $above_this_node; + + for ( $i = count( $this->stack ) - 1; $i >= 0; $i-- ) { + $node = $this->stack[ $i ]; + + if ( ! $has_found_node ) { + $has_found_node = $node === $above_this_node; + continue; + } + + yield $node; + } + } + + /* + * Internal helpers. + */ + + /** + * Updates internal flags after adding an element. + * + * Certain conditions (such as "has_p_in_button_scope") are maintained here as + * flags that are only modified when adding and removing elements. This allows + * the HTML Processor to quickly check for these conditions instead of iterating + * over the open stack elements upon each new tag it encounters. These flags, + * however, need to be maintained as items are added and removed from the stack. + * + * @since 6.4.0 + * + * @param WP_HTML_Token $item Element that was added to the stack of open elements. + */ + public function after_element_push( Gutenberg_HTML_Token_6_7 $item ): void { + /* + * When adding support for new elements, expand this switch to trap + * cases where the precalculated value needs to change. + */ + switch ( $item->node_name ) { + case 'BUTTON': + $this->has_p_in_button_scope = false; + break; + + case 'P': + $this->has_p_in_button_scope = true; + break; + } + + if ( null !== $this->push_handler ) { + ( $this->push_handler )( $item ); + } + } + + /** + * Updates internal flags after removing an element. + * + * Certain conditions (such as "has_p_in_button_scope") are maintained here as + * flags that are only modified when adding and removing elements. This allows + * the HTML Processor to quickly check for these conditions instead of iterating + * over the open stack elements upon each new tag it encounters. These flags, + * however, need to be maintained as items are added and removed from the stack. + * + * @since 6.4.0 + * + * @param WP_HTML_Token $item Element that was removed from the stack of open elements. + */ + public function after_element_pop( Gutenberg_HTML_Token_6_7 $item ): void { + /* + * When adding support for new elements, expand this switch to trap + * cases where the precalculated value needs to change. + */ + switch ( $item->node_name ) { + case 'BUTTON': + $this->has_p_in_button_scope = $this->has_element_in_button_scope( 'P' ); + break; + + case 'P': + $this->has_p_in_button_scope = $this->has_element_in_button_scope( 'P' ); + break; + } + + if ( null !== $this->pop_handler ) { + ( $this->pop_handler )( $item ); + } + } + + /** + * Wakeup magic method. + * + * @since 6.6.0 + */ + public function __wakeup() { + throw new \LogicException( __CLASS__ . ' should never be unserialized' ); + } +} diff --git a/lib/compat/wordpress-6.7/html-api/class-gutenberg-html-processor-6-7.php b/lib/compat/wordpress-6.7/html-api/class-gutenberg-html-processor-6-7.php new file mode 100644 index 0000000000000..15cbbdc6adda8 --- /dev/null +++ b/lib/compat/wordpress-6.7/html-api/class-gutenberg-html-processor-6-7.php @@ -0,0 +1,3402 @@ +next_tag( array( 'breadcrumbs' => array( 'DIV', 'FIGURE', 'IMG' ) ) ) ) { + * $processor->add_class( 'responsive-image' ); + * } + * + * #### Breadcrumbs + * + * Breadcrumbs represent the stack of open elements from the root + * of the document or fragment down to the currently-matched node, + * if one is currently selected. Call WP_HTML_Processor::get_breadcrumbs() + * to inspect the breadcrumbs for a matched tag. + * + * Breadcrumbs can specify nested HTML structure and are equivalent + * to a CSS selector comprising tag names separated by the child + * combinator, such as "DIV > FIGURE > IMG". + * + * Since all elements find themselves inside a full HTML document + * when parsed, the return value from `get_breadcrumbs()` will always + * contain any implicit outermost elements. For example, when parsing + * with `create_fragment()` in the `BODY` context (the default), any + * tag in the given HTML document will contain `array( 'HTML', 'BODY', … )` + * in its breadcrumbs. + * + * Despite containing the implied outermost elements in their breadcrumbs, + * tags may be found with the shortest-matching breadcrumb query. That is, + * `array( 'IMG' )` matches all IMG elements and `array( 'P', 'IMG' )` + * matches all IMG elements directly inside a P element. To ensure that no + * partial matches erroneously match it's possible to specify in a query + * the full breadcrumb match all the way down from the root HTML element. + * + * Example: + * + * $html = '
A lovely day outside
'; + * // ----- Matches here. + * $processor->next_tag( array( 'breadcrumbs' => array( 'FIGURE', 'IMG' ) ) ); + * + * $html = '
A lovely day outside
'; + * // ---- Matches here. + * $processor->next_tag( array( 'breadcrumbs' => array( 'FIGURE', 'FIGCAPTION', 'EM' ) ) ); + * + * $html = '
'; + * // ----- Matches here, because IMG must be a direct child of the implicit BODY. + * $processor->next_tag( array( 'breadcrumbs' => array( 'BODY', 'IMG' ) ) ); + * + * ## HTML Support + * + * This class implements a small part of the HTML5 specification. + * It's designed to operate within its support and abort early whenever + * encountering circumstances it can't properly handle. This is + * the principle way in which this class remains as simple as possible + * without cutting corners and breaking compliance. + * + * ### Supported elements + * + * If any unsupported element appears in the HTML input the HTML Processor + * will abort early and stop all processing. This draconian measure ensures + * that the HTML Processor won't break any HTML it doesn't fully understand. + * + * The following list specifies the HTML tags that _are_ supported: + * + * - Containers: ADDRESS, BLOCKQUOTE, DETAILS, DIALOG, DIV, FOOTER, HEADER, MAIN, MENU, SPAN, SUMMARY. + * - Custom elements: All custom elements are supported. :) + * - Form elements: BUTTON, DATALIST, FIELDSET, INPUT, LABEL, LEGEND, METER, OPTGROUP, OPTION, PROGRESS, SEARCH, SELECT. + * - Formatting elements: B, BIG, CODE, EM, FONT, I, PRE, SMALL, STRIKE, STRONG, TT, U, WBR. + * - Heading elements: H1, H2, H3, H4, H5, H6, HGROUP. + * - Links: A. + * - Lists: DD, DL, DT, LI, OL, UL. + * - Media elements: AUDIO, CANVAS, EMBED, FIGCAPTION, FIGURE, IMG, MAP, PICTURE, SOURCE, TRACK, VIDEO. + * - Paragraph: BR, P. + * - Phrasing elements: ABBR, AREA, BDI, BDO, CITE, DATA, DEL, DFN, INS, MARK, OUTPUT, Q, SAMP, SUB, SUP, TIME, VAR. + * - Sectioning elements: ARTICLE, ASIDE, HR, NAV, SECTION. + * - Templating elements: SLOT. + * - Text decoration: RUBY. + * - Deprecated elements: ACRONYM, BLINK, CENTER, DIR, ISINDEX, KEYGEN, LISTING, MULTICOL, NEXTID, PARAM, SPACER. + * + * ### Supported markup + * + * Some kinds of non-normative HTML involve reconstruction of formatting elements and + * re-parenting of mis-nested elements. For example, a DIV tag found inside a TABLE + * may in fact belong _before_ the table in the DOM. If the HTML Processor encounters + * such a case it will stop processing. + * + * The following list specifies HTML markup that _is_ supported: + * + * - Markup involving only those tags listed above. + * - Fully-balanced and non-overlapping tags. + * - HTML with unexpected tag closers. + * - Some unbalanced or overlapping tags. + * - P tags after unclosed P tags. + * - BUTTON tags after unclosed BUTTON tags. + * - A tags after unclosed A tags that don't involve any active formatting elements. + * + * @since 6.4.0 + * + * @see WP_HTML_Tag_Processor + * @see https://html.spec.whatwg.org/ + */ +class Gutenberg_HTML_Processor_6_7 extends Gutenberg_HTML_Tag_Processor_6_7 { + /** + * The maximum number of bookmarks allowed to exist at any given time. + * + * HTML processing requires more bookmarks than basic tag processing, + * so this class constant from the Tag Processor is overwritten. + * + * @since 6.4.0 + * + * @var int + */ + const MAX_BOOKMARKS = 100; + + /** + * Holds the working state of the parser, including the stack of + * open elements and the stack of active formatting elements. + * + * Initialized in the constructor. + * + * @since 6.4.0 + * + * @var WP_HTML_Processor_State + */ + private $state; + + /** + * Used to create unique bookmark names. + * + * This class sets a bookmark for every tag in the HTML document that it encounters. + * The bookmark name is auto-generated and increments, starting with `1`. These are + * internal bookmarks and are automatically released when the referring WP_HTML_Token + * goes out of scope and is garbage-collected. + * + * @since 6.4.0 + * + * @see WP_HTML_Processor::$release_internal_bookmark_on_destruct + * + * @var int + */ + private $bookmark_counter = 0; + + /** + * Stores an explanation for why something failed, if it did. + * + * @see self::get_last_error + * + * @since 6.4.0 + * + * @var string|null + */ + private $last_error = null; + + /** + * Stores context for why the parser bailed on unsupported HTML, if it did. + * + * @see self::get_unsupported_exception + * + * @since 6.7.0 + * + * @var WP_HTML_Unsupported_Exception|null + */ + private $unsupported_exception = null; + + /** + * Releases a bookmark when PHP garbage-collects its wrapping WP_HTML_Token instance. + * + * This function is created inside the class constructor so that it can be passed to + * the stack of open elements and the stack of active formatting elements without + * exposing it as a public method on the class. + * + * @since 6.4.0 + * + * @var Closure|null + */ + private $release_internal_bookmark_on_destruct = null; + + /** + * Stores stack events which arise during parsing of the + * HTML document, which will then supply the "match" events. + * + * @since 6.6.0 + * + * @var WP_HTML_Stack_Event[] + */ + private $element_queue = array(); + + /** + * Stores the current breadcrumbs. + * + * @since 6.7.0 + * + * @var string[] + */ + private $breadcrumbs = array(); + + /** + * Current stack event, if set, representing a matched token. + * + * Because the parser may internally point to a place further along in a document + * than the nodes which have already been processed (some "virtual" nodes may have + * appeared while scanning the HTML document), this will point at the "current" node + * being processed. It comes from the front of the element queue. + * + * @since 6.6.0 + * + * @var WP_HTML_Stack_Event|null + */ + private $current_element = null; + + /** + * Context node if created as a fragment parser. + * + * @var WP_HTML_Token|null + */ + private $context_node = null; + + /** + * Whether the parser has yet processed the context node, + * if created as a fragment parser. + * + * The context node will be initially pushed onto the stack of open elements, + * but when created as a fragment parser, this context element (and the implicit + * HTML document node above it) should not be exposed as a matched token or node. + * + * This boolean indicates whether the processor should skip over the current + * node in its initial search for the first node created from the input HTML. + * + * @var bool + */ + private $has_seen_context_node = false; + + /* + * Public Interface Functions + */ + + /** + * Creates an HTML processor in the fragment parsing mode. + * + * Use this for cases where you are processing chunks of HTML that + * will be found within a bigger HTML document, such as rendered + * block output that exists within a post, `the_content` inside a + * rendered site layout. + * + * Fragment parsing occurs within a context, which is an HTML element + * that the document will eventually be placed in. It becomes important + * when special elements have different rules than others, such as inside + * a TEXTAREA or a TITLE tag where things that look like tags are text, + * or inside a SCRIPT tag where things that look like HTML syntax are JS. + * + * The context value should be a representation of the tag into which the + * HTML is found. For most cases this will be the body element. The HTML + * form is provided because a context element may have attributes that + * impact the parse, such as with a SCRIPT tag and its `type` attribute. + * + * ## Current HTML Support + * + * - The only supported context is ``, which is the default value. + * - The only supported document encoding is `UTF-8`, which is the default value. + * + * @since 6.4.0 + * @since 6.6.0 Returns `static` instead of `self` so it can create subclass instances. + * + * @param string $html Input HTML fragment to process. + * @param string $context Context element for the fragment, must be default of ``. + * @param string $encoding Text encoding of the document; must be default of 'UTF-8'. + * @return static|null The created processor if successful, otherwise null. + */ + public static function create_fragment( $html, $context = '', $encoding = 'UTF-8' ) { + if ( '' !== $context || 'UTF-8' !== $encoding ) { + return null; + } + + $processor = new static( $html, self::CONSTRUCTOR_UNLOCK_CODE ); + $processor->state->context_node = array( 'BODY', array() ); + $processor->state->insertion_mode = Gutenberg_HTML_Processor_State_6_7::INSERTION_MODE_IN_BODY; + + // @todo Create "fake" bookmarks for non-existent but implied nodes. + $processor->bookmarks['root-node'] = new Gutenberg_HTML_Span_6_7( 0, 0 ); + $processor->bookmarks['context-node'] = new Gutenberg_HTML_Span_6_7( 0, 0 ); + + $processor->state->stack_of_open_elements->push( + new Gutenberg_HTML_Token_6_7( + 'root-node', + 'HTML', + false + ) + ); + + $context_node = new Gutenberg_HTML_Token_6_7( + 'context-node', + $processor->state->context_node[0], + false + ); + + $processor->context_node = $context_node; + $processor->breadcrumbs = array( 'HTML', $context_node->node_name ); + + return $processor; + } + + /** + * Constructor. + * + * Do not use this method. Use the static creator methods instead. + * + * @access private + * + * @since 6.4.0 + * + * @see WP_HTML_Processor::create_fragment() + * + * @param string $html HTML to process. + * @param string|null $use_the_static_create_methods_instead This constructor should not be called manually. + */ + public function __construct( $html, $use_the_static_create_methods_instead = null ) { + parent::__construct( $html ); + + if ( self::CONSTRUCTOR_UNLOCK_CODE !== $use_the_static_create_methods_instead ) { + _doing_it_wrong( + __METHOD__, + sprintf( + /* translators: %s: WP_HTML_Processor::create_fragment(). */ + __( 'Call %s to create an HTML Processor instead of calling the constructor directly.' ), + 'WP_HTML_Processor::create_fragment()' + ), + '6.4.0' + ); + } + + $this->state = new Gutenberg_HTML_Processor_State_6_7(); + + $this->state->stack_of_open_elements->set_push_handler( + function ( Gutenberg_HTML_Token_6_7 $token ): void { + $is_virtual = ! isset( $this->state->current_token ) || $this->is_tag_closer(); + $same_node = isset( $this->state->current_token ) && $token->node_name === $this->state->current_token->node_name; + $provenance = ( ! $same_node || $is_virtual ) ? 'virtual' : 'real'; + $this->element_queue[] = new Gutenberg_HTML_Stack_Event_6_7( $token, Gutenberg_HTML_Stack_Event_6_7::PUSH, $provenance ); + } + ); + + $this->state->stack_of_open_elements->set_pop_handler( + function ( Gutenberg_HTML_Token_6_7 $token ): void { + $is_virtual = ! isset( $this->state->current_token ) || ! $this->is_tag_closer(); + $same_node = isset( $this->state->current_token ) && $token->node_name === $this->state->current_token->node_name; + $provenance = ( ! $same_node || $is_virtual ) ? 'virtual' : 'real'; + $this->element_queue[] = new Gutenberg_HTML_Stack_Event_6_7( $token, Gutenberg_HTML_Stack_Event_6_7::POP, $provenance ); + } + ); + + /* + * Create this wrapper so that it's possible to pass + * a private method into WP_HTML_Token classes without + * exposing it to any public API. + */ + $this->release_internal_bookmark_on_destruct = function ( string $name ): void { + parent::release_bookmark( $name ); + }; + } + + /** + * Stops the parser and terminates its execution when encountering unsupported markup. + * + * @throws WP_HTML_Unsupported_Exception Halts execution of the parser. + * + * @since 6.7.0 + * + * @param string $message Explains support is missing in order to parse the current node. + */ + private function bail( string $message ) { + $here = $this->bookmarks[ $this->state->current_token->bookmark_name ]; + $token = substr( $this->html, $here->start, $here->length ); + + $open_elements = array(); + foreach ( $this->state->stack_of_open_elements->stack as $item ) { + $open_elements[] = $item->node_name; + } + + $active_formats = array(); + foreach ( $this->state->active_formatting_elements->walk_down() as $item ) { + $active_formats[] = $item->node_name; + } + + $this->last_error = self::ERROR_UNSUPPORTED; + + $this->unsupported_exception = new Gutenberg_HTML_Unsupported_Exception_6_7( + $message, + $this->state->current_token->node_name, + $here->start, + $token, + $open_elements, + $active_formats + ); + + throw $this->unsupported_exception; + } + + /** + * Returns the last error, if any. + * + * Various situations lead to parsing failure but this class will + * return `false` in all those cases. To determine why something + * failed it's possible to request the last error. This can be + * helpful to know to distinguish whether a given tag couldn't + * be found or if content in the document caused the processor + * to give up and abort processing. + * + * Example + * + * $processor = WP_HTML_Processor::create_fragment( '