From 62704a0167913527203e48e5c7d5a2dda575f44d Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:59:41 +0200 Subject: [PATCH 01/31] ESLint: Bump `eslint-plugin-react-compiler` to latest beta (#67106) * Bump eslint-plugin-react-compiler to 19.0.0-beta-0dec889-20241115 * Use ref for prev heading level in DocumentOutline Co-authored-by: tyxla Co-authored-by: Mamaduka --- package-lock.json | 8 ++++---- package.json | 2 +- .../editor/src/components/document-outline/index.js | 10 ++++++---- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 036acafe885fa6..376cd0b9e6edf6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -91,7 +91,7 @@ "eslint-plugin-jest": "27.2.3", "eslint-plugin-jest-dom": "5.0.2", "eslint-plugin-prettier": "5.0.0", - "eslint-plugin-react-compiler": "19.0.0-beta-8a03594-20241020", + "eslint-plugin-react-compiler": "19.0.0-beta-0dec889-20241115", "eslint-plugin-ssr-friendly": "1.0.6", "eslint-plugin-storybook": "0.6.13", "eslint-plugin-testing-library": "6.0.2", @@ -25365,9 +25365,9 @@ } }, "node_modules/eslint-plugin-react-compiler": { - "version": "19.0.0-beta-8a03594-20241020", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-19.0.0-beta-8a03594-20241020.tgz", - "integrity": "sha512-bYg1COih1s3r14IV/AKdQs/SN7CQmNI0ZaMtPdgZ6gp1S1Q/KGP9P43w7R6dHJ4wYpuMBvekNJHQdVu+x6UM+A==", + "version": "19.0.0-beta-0dec889-20241115", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-19.0.0-beta-0dec889-20241115.tgz", + "integrity": "sha512-jTjEHuE8/R6qD/CD2d+5YvWMy1q9/tX3kft4WDyg42/HktjHtHXrEToyZ6THEQf8t/YWMY1RGeCkykePbACtFA==", "dev": true, "dependencies": { "@babel/core": "^7.24.4", diff --git a/package.json b/package.json index 751bde40240811..a7470a1333ffad 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "eslint-plugin-jest": "27.2.3", "eslint-plugin-jest-dom": "5.0.2", "eslint-plugin-prettier": "5.0.0", - "eslint-plugin-react-compiler": "19.0.0-beta-8a03594-20241020", + "eslint-plugin-react-compiler": "19.0.0-beta-0dec889-20241115", "eslint-plugin-ssr-friendly": "1.0.6", "eslint-plugin-storybook": "0.6.13", "eslint-plugin-testing-library": "6.0.2", diff --git a/packages/editor/src/components/document-outline/index.js b/packages/editor/src/components/document-outline/index.js index ab5caa8cf0f8a4..c5e59837362092 100644 --- a/packages/editor/src/components/document-outline/index.js +++ b/packages/editor/src/components/document-outline/index.js @@ -3,6 +3,7 @@ */ import { __ } from '@wordpress/i18n'; import { useDispatch, useSelect } from '@wordpress/data'; +import { useRef } from '@wordpress/element'; import { create, getTextContent } from '@wordpress/rich-text'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; @@ -125,6 +126,8 @@ export default function DocumentOutline( { }; } ); + const prevHeadingLevelRef = useRef( 1 ); + const headings = computeOutlineHeadings( blocks ); if ( headings.length < 1 ) { return ( @@ -139,8 +142,6 @@ export default function DocumentOutline( { ); } - let prevHeadingLevel = 1; - // Not great but it's the simplest way to locate the title right now. const titleNode = document.querySelector( '.editor-post-title__input' ); const hasTitle = isTitleSupported && title && titleNode; @@ -170,7 +171,8 @@ export default function DocumentOutline( { { headings.map( ( item, index ) => { // Headings remain the same, go up by one, or down by any amount. // Otherwise there are missing levels. - const isIncorrectLevel = item.level > prevHeadingLevel + 1; + const isIncorrectLevel = + item.level > prevHeadingLevelRef.current + 1; const isValid = ! item.isEmpty && @@ -178,7 +180,7 @@ export default function DocumentOutline( { !! item.level && ( item.level !== 1 || ( ! hasMultipleH1 && ! hasTitle ) ); - prevHeadingLevel = item.level; + prevHeadingLevelRef.current = item.level; return ( Date: Tue, 19 Nov 2024 09:12:00 -0500 Subject: [PATCH 02/31] Bump gradle/actions from 4.2.0 to 4.2.1 in the react-native group (#67115) Bumps the react-native group with 1 update: [gradle/actions](https://github.com/gradle/actions). Updates `gradle/actions` from 4.2.0 to 4.2.1 - [Release notes](https://github.com/gradle/actions/releases) - [Commits](https://github.com/gradle/actions/compare/473878a77f1b98e2b5ac4af93489d1656a80a5ed...cc4fc85e6b35bafd578d5ffbc76a5518407e1af0) --- updated-dependencies: - dependency-name: gradle/actions dependency-type: direct:production update-type: version-update:semver-patch dependency-group: react-native ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: desrosj --- .github/workflows/rnmobile-android-runner.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rnmobile-android-runner.yml b/.github/workflows/rnmobile-android-runner.yml index 43a71809b5bbe7..f8ff0441a95b7b 100644 --- a/.github/workflows/rnmobile-android-runner.yml +++ b/.github/workflows/rnmobile-android-runner.yml @@ -47,7 +47,7 @@ jobs: run: npm run native test:e2e:setup - name: Gradle cache - uses: gradle/actions/setup-gradle@473878a77f1b98e2b5ac4af93489d1656a80a5ed # v4.2.0 + uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1 # AVD cache disabled as it caused emulator termination to hang indefinitely. # https://github.com/ReactiveCircus/android-emulator-runner/issues/385 From cf59c28e1b0cd0870eb82c21eedf221ebf92c606 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 19 Nov 2024 18:24:22 +0400 Subject: [PATCH 03/31] Inserter: Set initial active tab ID during render (#67103) Co-authored-by: Mamaduka Co-authored-by: tyxla --- .../components/inserter/category-tabs/index.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/block-editor/src/components/inserter/category-tabs/index.js b/packages/block-editor/src/components/inserter/category-tabs/index.js index 7b6baaab398f82..ff0a130f1a8271 100644 --- a/packages/block-editor/src/components/inserter/category-tabs/index.js +++ b/packages/block-editor/src/components/inserter/category-tabs/index.js @@ -6,7 +6,7 @@ import { privateApis as componentsPrivateApis, __unstableMotion as motion, } from '@wordpress/components'; -import { useState, useEffect } from '@wordpress/element'; +import { useState } from '@wordpress/element'; /** * Internal dependencies @@ -35,14 +35,13 @@ function CategoryTabs( { const selectedTabId = selectedCategory ? selectedCategory.name : null; const [ activeTabId, setActiveId ] = useState(); const firstTabId = categories?.[ 0 ]?.name; - useEffect( () => { - // If there is no active tab, make the first tab the active tab, so that - // when focus is moved to the tablist, the first tab will be focused - // despite not being selected - if ( selectedTabId === null && ! activeTabId && firstTabId ) { - setActiveId( firstTabId ); - } - }, [ selectedTabId, activeTabId, firstTabId, setActiveId ] ); + + // If there is no active tab, make the first tab the active tab, so that + // when focus is moved to the tablist, the first tab will be focused + // despite not being selected + if ( selectedTabId === null && ! activeTabId && firstTabId ) { + setActiveId( firstTabId ); + } return ( Date: Tue, 19 Nov 2024 10:33:59 -0500 Subject: [PATCH 04/31] Feature: Set editor rendering mode by post type (#62304) Co-authored-by: TylerB24890 Co-authored-by: Sidsector9 Co-authored-by: fabiankaegy Co-authored-by: youknowriad Co-authored-by: dcalhoun Co-authored-by: ramonjd Co-authored-by: mcsf Co-authored-by: jasmussen Co-authored-by: annezazu Co-authored-by: jameskoster Co-authored-by: dinhtungdu --- backport-changelog/6.8/7129.md | 3 + ...tenberg-rest-post-types-controller-6-8.php | 61 ++++++++++++++ lib/compat/wordpress-6.8/post.php | 57 +++++++++++++ lib/compat/wordpress-6.8/rest-api.php | 22 +++++ lib/load.php | 3 + .../block-editor/use-site-editor-settings.js | 11 +-- .../edit-site/src/components/editor/index.js | 4 +- .../editor/src/components/provider/index.js | 83 ++++++++++++------- packages/editor/src/store/reducer.native.js | 2 + .../initialize-editor.js | 17 ++++ test/performance/fixtures/perf-utils.ts | 20 +++++ test/performance/specs/site-editor.spec.js | 2 + 12 files changed, 243 insertions(+), 42 deletions(-) create mode 100644 backport-changelog/6.8/7129.md create mode 100644 lib/compat/wordpress-6.8/class-gutenberg-rest-post-types-controller-6-8.php create mode 100644 lib/compat/wordpress-6.8/post.php create mode 100644 lib/compat/wordpress-6.8/rest-api.php diff --git a/backport-changelog/6.8/7129.md b/backport-changelog/6.8/7129.md new file mode 100644 index 00000000000000..90c9168cdc6f8a --- /dev/null +++ b/backport-changelog/6.8/7129.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7129 + +* https://github.com/WordPress/gutenberg/pull/62304 diff --git a/lib/compat/wordpress-6.8/class-gutenberg-rest-post-types-controller-6-8.php b/lib/compat/wordpress-6.8/class-gutenberg-rest-post-types-controller-6-8.php new file mode 100644 index 00000000000000..da0489210e21f1 --- /dev/null +++ b/lib/compat/wordpress-6.8/class-gutenberg-rest-post-types-controller-6-8.php @@ -0,0 +1,61 @@ +default_rendering_mode, $item ); + + /** + * Filters the block editor rendering mode for a specific post type. + * Applied after the generic `post_type_default_rendering_mode` filter. + * + * The dynamic portion of the hook name, `$item->name`, refers to the post type slug. + * + * @since 6.8.0 + * @param string $default_rendering_mode Default rendering mode for the post type. + * @param WP_Post_Type $post_type Post type object. + * @return string Default rendering mode for the post type. + */ + $rendering_mode = apply_filters( "post_type_{$item->name}_default_rendering_mode", $rendering_mode, $item ); + + // Validate the filtered rendering mode. + if ( ! in_array( $rendering_mode, gutenberg_post_type_rendering_modes(), true ) ) { + $rendering_mode = 'post-only'; + } + + $response->data['default_rendering_mode'] = $rendering_mode; + } + + return rest_ensure_response( $response ); + } +} diff --git a/lib/compat/wordpress-6.8/post.php b/lib/compat/wordpress-6.8/post.php new file mode 100644 index 00000000000000..26e6c3adc07a3d --- /dev/null +++ b/lib/compat/wordpress-6.8/post.php @@ -0,0 +1,57 @@ +register_routes(); + } +} +add_action( 'rest_api_init', 'gutenberg_add_post_type_rendering_mode' ); diff --git a/lib/load.php b/lib/load.php index d7e4a33cd02c92..100160176f3911 100644 --- a/lib/load.php +++ b/lib/load.php @@ -49,6 +49,8 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.8 compat. require __DIR__ . '/compat/wordpress-6.8/block-comments.php'; require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-comment-controller-6-8.php'; + require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-post-types-controller-6-8.php'; + require __DIR__ . '/compat/wordpress-6.8/rest-api.php'; // Plugin specific code. require_once __DIR__ . '/class-wp-rest-global-styles-controller-gutenberg.php'; @@ -120,6 +122,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.8/preload.php'; require __DIR__ . '/compat/wordpress-6.8/blocks.php'; require __DIR__ . '/compat/wordpress-6.8/functions.php'; +require __DIR__ . '/compat/wordpress-6.8/post.php'; // Experimental features. require __DIR__ . '/experimental/block-editor-settings-mobile.php'; diff --git a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js index 5c75de6d81e720..186f4aacf79232 100644 --- a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js +++ b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js @@ -36,9 +36,7 @@ function useNavigateToPreviousEntityRecord() { return goBack; } -export function useSpecificEditorSettings( - shouldUseTemplateAsDefaultRenderingMode -) { +export function useSpecificEditorSettings() { const { params } = useLocation(); const { canvas = 'view' } = params; const onNavigateToEntityRecord = useNavigateToEntityRecord(); @@ -49,11 +47,6 @@ export function useSpecificEditorSettings( }; }, [] ); - // TODO: The `shouldUseTemplateAsDefaultRenderingMode` check should be removed when the default rendering mode per post type is merged. - // @see https://github.com/WordPress/gutenberg/pull/62304/ - const defaultRenderingMode = shouldUseTemplateAsDefaultRenderingMode - ? 'template-locked' - : 'post-only'; const onNavigateToPreviousEntityRecord = useNavigateToPreviousEntityRecord(); const defaultEditorSettings = useMemo( () => { @@ -63,7 +56,6 @@ export function useSpecificEditorSettings( richEditingEnabled: true, supportsTemplateMode: true, focusMode: canvas !== 'view', - defaultRenderingMode, onNavigateToEntityRecord, onNavigateToPreviousEntityRecord, isPreviewMode: canvas === 'view', @@ -71,7 +63,6 @@ export function useSpecificEditorSettings( }, [ settings, canvas, - defaultRenderingMode, onNavigateToEntityRecord, onNavigateToPreviousEntityRecord, ] ); diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index 51d734f25c6adb..1d115dca7518df 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -131,9 +131,7 @@ export default function EditSiteEditor( { isPostsList = false } ) { 'edit-site-editor__loading-progress' ); - const settings = useSpecificEditorSettings( - !! context?.postId && context?.postType !== 'post' - ); + const settings = useSpecificEditorSettings(); const styles = useMemo( () => [ ...settings.styles, diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index 50d02610062c00..6c05e5b58235b3 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -72,8 +72,7 @@ const NON_CONTEXTUAL_POST_TYPES = [ * @return {Array} Block editor props. */ function useBlockEditorProps( post, template, mode ) { - const rootLevelPost = - mode === 'post-only' || ! template ? 'post' : 'template'; + const rootLevelPost = mode === 'template-locked' ? 'template' : 'post'; const [ postBlocks, onInput, onChange ] = useEntityBlockEditor( 'postType', post.type, @@ -164,30 +163,48 @@ export const ExperimentalEditorProvider = withRegistryProvider( BlockEditorProviderComponent = ExperimentalBlockEditorProvider, __unstableTemplate: template, } ) => { - const { editorSettings, selection, isReady, mode, postTypeEntities } = - useSelect( - ( select ) => { - const { - getEditorSettings, - getEditorSelection, - getRenderingMode, - __unstableIsEditorReady, - } = select( editorStore ); - const { getEntitiesConfig } = select( coreStore ); + const { + editorSettings, + selection, + isReady, + mode, + defaultMode, + postTypeEntities, + hasLoadedPostObject, + } = useSelect( + ( select ) => { + const { + getEditorSettings, + getEditorSelection, + getRenderingMode, + __unstableIsEditorReady, + } = select( editorStore ); + const { getEntitiesConfig } = select( coreStore ); + + const postTypeObject = select( coreStore ).getPostType( + post.type + ); + + const _hasLoadedPostObject = select( + coreStore + ).hasFinishedResolution( 'getPostType', [ post.type ] ); - return { - editorSettings: getEditorSettings(), - isReady: __unstableIsEditorReady(), - mode: getRenderingMode(), - selection: getEditorSelection(), - postTypeEntities: - post.type === 'wp_template' - ? getEntitiesConfig( 'postType' ) - : null, - }; - }, - [ post.type ] - ); + return { + hasLoadedPostObject: _hasLoadedPostObject, + editorSettings: getEditorSettings(), + isReady: __unstableIsEditorReady(), + mode: getRenderingMode(), + defaultMode: + postTypeObject?.default_rendering_mode ?? 'post-only', + selection: getEditorSelection(), + postTypeEntities: + post.type === 'wp_template' + ? getEntitiesConfig( 'postType' ) + : null, + }; + }, + [ post.type ] + ); const shouldRenderTemplate = !! template && mode !== 'post-only'; const rootLevelPost = shouldRenderTemplate ? template : post; const defaultBlockContext = useMemo( () => { @@ -282,7 +299,15 @@ export const ExperimentalEditorProvider = withRegistryProvider( } ); } - }, [] ); + }, [ + createWarningNotice, + initialEdits, + settings, + post, + recovery, + setupEditor, + updatePostLock, + ] ); // Synchronizes the active post with the state useEffect( () => { @@ -301,15 +326,15 @@ export const ExperimentalEditorProvider = withRegistryProvider( // Sets the right rendering mode when loading the editor. useEffect( () => { - setRenderingMode( settings.defaultRenderingMode ?? 'post-only' ); - }, [ settings.defaultRenderingMode, setRenderingMode ] ); + setRenderingMode( defaultMode ); + }, [ defaultMode, setRenderingMode ] ); useHideBlocksFromInserter( post.type, mode ); // Register the editor commands. useCommands(); - if ( ! isReady ) { + if ( ! isReady || ! mode || ! hasLoadedPostObject ) { return null; } diff --git a/packages/editor/src/store/reducer.native.js b/packages/editor/src/store/reducer.native.js index 7566dfc5dfd038..fbf6c968f57d08 100644 --- a/packages/editor/src/store/reducer.native.js +++ b/packages/editor/src/store/reducer.native.js @@ -9,6 +9,7 @@ import { combineReducers } from '@wordpress/data'; import { postId, postType, + renderingMode, saving, postLock, postSavingLock, @@ -82,6 +83,7 @@ export default combineReducers( { postId, postType, postTitle, + renderingMode, saving, postLock, postSavingLock, diff --git a/test/native/integration-test-helpers/initialize-editor.js b/test/native/integration-test-helpers/initialize-editor.js index 511f0223e11356..3b89da979aee3a 100644 --- a/test/native/integration-test-helpers/initialize-editor.js +++ b/test/native/integration-test-helpers/initialize-editor.js @@ -10,6 +10,8 @@ import { v4 as uuid } from 'uuid'; import { createElement, cloneElement } from '@wordpress/element'; // eslint-disable-next-line no-restricted-imports import { initializeEditor as internalInitializeEditor } from '@wordpress/edit-post'; +import { store as coreStore } from '@wordpress/core-data'; +import { select } from '@wordpress/data'; /** * Internal dependencies @@ -28,6 +30,21 @@ import { getGlobalStyles } from './get-global-styles'; * @return {import('@testing-library/react-native').RenderAPI} A Testing Library screen. */ export async function initializeEditor( props, { component } = {} ) { + const resolutionSpy = jest.spyOn( + select( coreStore ), + 'hasFinishedResolution' + ); + const actualResolution = resolutionSpy.getMockImplementation(); + resolutionSpy.mockImplementation( ( selectorName, args ) => { + // The mobile editor only supports the `post-only` rendering mode, so we + // presume a resolved `getPostType` selector to unblock editor rendering. + if ( 'getPostType' === selectorName ) { + return true; + } + + return actualResolution( selectorName, args ); + } ); + const uniqueId = uuid(); const postId = `post-id-${ uniqueId }`; const postType = 'post'; diff --git a/test/performance/fixtures/perf-utils.ts b/test/performance/fixtures/perf-utils.ts index 592e8194852e3b..8d23d91ff91bfd 100644 --- a/test/performance/fixtures/perf-utils.ts +++ b/test/performance/fixtures/perf-utils.ts @@ -97,6 +97,26 @@ export class PerfUtils { return canvas; } + /** + * Change the rendering mode of the editor. + * + * Setting the rendering mode to something other than the default is sometimes + * needed when for example we want to update the contents of the editor from a + * HTML file. Calling the resetBlocks method of the core/block-editor store will + * replace the contents of the template if the rendering mode is not post-only. + * So this should always be called before the resetBlocks method is used. + * + * @param newRenderingMode Rendering mode to set + * + * @return Promise + */ + async setRenderingMode( newRenderingMode: string ) { + await this.page.evaluate( ( _newRenderingMode ) => { + const { dispatch } = window.wp.data; + dispatch( 'core/editor' ).setRenderingMode( _newRenderingMode ); + }, newRenderingMode ); + } + /** * Loads blocks from the small post with containers fixture into the editor * canvas. diff --git a/test/performance/specs/site-editor.spec.js b/test/performance/specs/site-editor.spec.js index 9c9d8aec71da4a..e72d83fa8b3aa4 100644 --- a/test/performance/specs/site-editor.spec.js +++ b/test/performance/specs/site-editor.spec.js @@ -64,6 +64,7 @@ test.describe( 'Site Editor Performance', () => { test( 'Setup the test page', async ( { admin, perfUtils } ) => { await admin.createNewPost( { postType: 'page' } ); + await perfUtils.setRenderingMode( 'post-only' ); await perfUtils.loadBlocksForLargePost(); draftId = await perfUtils.saveDraft(); @@ -122,6 +123,7 @@ test.describe( 'Site Editor Performance', () => { test( 'Setup the test post', async ( { admin, editor, perfUtils } ) => { await admin.createNewPost( { postType: 'page' } ); + await perfUtils.setRenderingMode( 'post-only' ); await perfUtils.loadBlocksForLargePost(); await editor.insertBlock( { name: 'core/paragraph' } ); From d478c08d4c0827864e7be4e2df487297ad36d7b0 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Wed, 20 Nov 2024 01:52:35 +0900 Subject: [PATCH 05/31] ColorPicker: Update sizes of format select and copy button (#67093) * ColorPicker: Update sizes of format select and copy button * Add changelog Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 4 ++++ .../src/color-picker/color-copy-button.tsx | 6 +++--- packages/components/src/color-picker/component.tsx | 1 + packages/components/src/color-picker/styles.ts | 13 ------------- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index d93eb30ac0ec50..dfb9454d8da1f0 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -12,6 +12,10 @@ - `ToggleGroupControl`: Fix active background for `0` value ([#66855](https://github.com/WordPress/gutenberg/pull/66855)). - `SlotFill`: Fix a bug where a stale value of `fillProps` could be used ([#67000](https://github.com/WordPress/gutenberg/pull/67000)). +### Enhancements + +- `ColorPicker`: Update sizes of color format select and copy button ([#67093](https://github.com/WordPress/gutenberg/pull/67093)). + ### Experimental - `SlotFill`: Remove registration API methods from return value of `__experimentalUseSlot` ([#67070](https://github.com/WordPress/gutenberg/pull/67070)). diff --git a/packages/components/src/color-picker/color-copy-button.tsx b/packages/components/src/color-picker/color-copy-button.tsx index b8a4822544322c..0076a2125d00c0 100644 --- a/packages/components/src/color-picker/color-copy-button.tsx +++ b/packages/components/src/color-picker/color-copy-button.tsx @@ -9,7 +9,7 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { CopyButton } from './styles'; +import { Button } from '../button'; import Tooltip from '../tooltip'; import type { ColorCopyButtonProps } from './types'; @@ -63,8 +63,8 @@ export const ColorCopyButton = ( props: ColorCopyButtonProps ) => { copiedColor === color.toHex() ? __( 'Copied!' ) : __( 'Copy' ) } > - diff --git a/packages/components/src/color-picker/styles.ts b/packages/components/src/color-picker/styles.ts index a78f10de2e4a32..50ce33da9f2333 100644 --- a/packages/components/src/color-picker/styles.ts +++ b/packages/components/src/color-picker/styles.ts @@ -11,7 +11,6 @@ import InnerSelectControl from '../select-control'; import InnerRangeControl from '../range-control'; import { space } from '../utils/space'; import { boxSizingReset } from '../utils'; -import Button from '../button'; import { Flex } from '../flex'; import { HStack } from '../h-stack'; import CONFIG from '../utils/config-values'; @@ -22,7 +21,6 @@ export const NumberControlWrapper = styled( NumberControl )` export const SelectControl = styled( InnerSelectControl )` margin-left: ${ space( -2 ) }; - width: 5em; `; export const RangeControl = styled( InnerRangeControl )` @@ -101,14 +99,3 @@ export const ColorfulWrapper = styled.div` ${ interactiveHueStyles } `; - -export const CopyButton = styled( Button )` - &&&&& { - min-width: ${ space( 6 ) }; - padding: 0; - - > svg { - margin-right: 0; - } - } -`; From dea308b86eb32c23d384cd2d0f756fc9a047cca1 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Wed, 20 Nov 2024 07:28:07 +0900 Subject: [PATCH 06/31] ColorPicker: Add accessible label for copy button (#67094) * ColorPicker: Add accessible label for copy button * Add changelog * Use variable Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 1 + .../src/color-picker/color-copy-button.tsx | 12 +++++------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index dfb9454d8da1f0..34a90b6ca23b3c 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -11,6 +11,7 @@ - `ToggleGroupControl`: Fix active background for `0` value ([#66855](https://github.com/WordPress/gutenberg/pull/66855)). - `SlotFill`: Fix a bug where a stale value of `fillProps` could be used ([#67000](https://github.com/WordPress/gutenberg/pull/67000)). +- `ColorPicker`: Add accessible label for copy button ([#67094](https://github.com/WordPress/gutenberg/pull/67094)). ### Enhancements diff --git a/packages/components/src/color-picker/color-copy-button.tsx b/packages/components/src/color-picker/color-copy-button.tsx index 0076a2125d00c0..6e49fa7ae85e74 100644 --- a/packages/components/src/color-picker/color-copy-button.tsx +++ b/packages/components/src/color-picker/color-copy-button.tsx @@ -55,16 +55,14 @@ export const ColorCopyButton = ( props: ColorCopyButtonProps ) => { }; }, [] ); + const label = + copiedColor === color.toHex() ? __( 'Copied!' ) : __( 'Copy' ); + return ( - + - - ); - } ); - } + { __( 'Edit' ) } + + + ); + } ); + return ( - - - { __( 'Edit track' ) } - - - { __( 'File' ) }: { fileName } - - - - onChange( { - ...track, - label: newLabel, - } ) - } - label={ __( 'Label' ) } - value={ label } - help={ __( 'Title of track' ) } - /> - + + { __( 'Edit track' ) } + + + { __( 'File' ) }: { fileName } + + + + onChange( { + ...track, + label: newLabel, + } ) + } + label={ __( 'Label' ) } + value={ label } + help={ __( 'Title of track' ) } + /> + + onChange( { + ...track, + srcLang: newSrcLang, + } ) + } + label={ __( 'Source language' ) } + value={ srcLang } + help={ __( 'Language tag (en, fr, etc.)' ) } + /> + + + { + onChange( { + ...track, + kind: newKind, + } ); + } } + /> + + + - - - + > + { __( 'Apply' ) } + + - + ); } @@ -194,6 +179,11 @@ export default function TracksEditor( { tracks = [], onChange } ) { return select( blockEditorStore ).getSettings().mediaUpload; }, [] ); const [ trackBeingEdited, setTrackBeingEdited ] = useState( null ); + const dropdownPopoverRef = useRef(); + + useEffect( () => { + dropdownPopoverRef.current?.focus(); + }, [ trackBeingEdited ] ); if ( ! mediaUpload ) { return null; @@ -201,17 +191,32 @@ export default function TracksEditor( { tracks = [], onChange } ) { return ( ( - - - { __( 'Text tracks' ) } - - - ) } + focusOnMount + popoverProps={ { + ref: dropdownPopoverRef, + } } + renderToggle={ ( { isOpen, onToggle } ) => { + const handleOnToggle = () => { + if ( ! isOpen ) { + // When the Popover opens make sure the initial view is + // always the track list rather than the edit track UI. + setTrackBeingEdited( null ); + } + onToggle(); + }; + + return ( + + + { __( 'Text tracks' ) } + + + ); + } } renderContent={ () => { if ( trackBeingEdited !== null ) { return ( @@ -235,8 +240,21 @@ export default function TracksEditor( { tracks = [], onChange } ) { /> ); } + return ( <> + { tracks.length === 0 && ( +
+

+ { __( 'Text tracks' ) } +

+

+ { __( + 'Tracks can be subtitles, captions, chapters, or descriptions. They help make your content more accessible to a wider range of users.' + ) } +

+
+ ) } Date: Wed, 20 Nov 2024 15:44:36 +0530 Subject: [PATCH 16/31] Blocks: Adds check for parent type before showing convert to pattern button (#66158) Co-authored-by: vipul0425 Co-authored-by: gziolo Co-authored-by: ntsekouras Co-authored-by: Mamaduka Co-authored-by: mtias Co-authored-by: aristath Co-authored-by: jordesign Co-authored-by: fabiankaegy --- .../src/components/pattern-convert-button.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/patterns/src/components/pattern-convert-button.js b/packages/patterns/src/components/pattern-convert-button.js index d670cd85946aa9..4573a6a5de4e82 100644 --- a/packages/patterns/src/components/pattern-convert-button.js +++ b/packages/patterns/src/components/pattern-convert-button.js @@ -6,6 +6,7 @@ import { isReusableBlock, createBlock, serialize, + getBlockType, } from '@wordpress/blocks'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { useState, useCallback } from '@wordpress/element'; @@ -60,6 +61,15 @@ export default function PatternConvertButton( { const blocks = getBlocksByClientId( clientIds ) ?? []; + // Check if the block has reusable support defined. + const hasReusableBlockSupport = ( blockName ) => { + const blockType = getBlockType( blockName ); + const hasParent = blockType && 'parent' in blockType; + + // If the block has a parent, check with false as default, otherwise with true. + return hasBlockSupport( blockName, 'reusable', ! hasParent ); + }; + const isReusable = blocks.length === 1 && blocks[ 0 ] && @@ -82,7 +92,7 @@ export default function PatternConvertButton( { // Hide on invalid blocks. block.isValid && // Hide when block doesn't support being made into a pattern. - hasBlockSupport( block.name, 'reusable', true ) + hasReusableBlockSupport( block.name ) ) && // Hide when current doesn't have permission to do that. // Blocks refers to the wp_block post type, this checks the ability to create a post of that type. From b408e17a44bec0660ff7d68f1327b9b520688b13 Mon Sep 17 00:00:00 2001 From: Juan Aldasoro Date: Wed, 20 Nov 2024 11:41:50 +0100 Subject: [PATCH 17/31] ColorPalette: Disable `Clear` button if there's no color value. (#67108) * Disable `Clear` button if there's no color value. * Add changelog entry. * Add PR to changelog entry. * Set gradient and duotone clear button as disabled if there's no value. Add `accessibleWhenDisabled` for all the changes. Co-authored-by: juanfra Co-authored-by: ntsekouras Co-authored-by: afercia --- packages/components/CHANGELOG.md | 3 +++ packages/components/src/color-palette/index.tsx | 6 +++++- packages/components/src/duotone-picker/duotone-picker.tsx | 2 ++ packages/components/src/gradient-picker/index.tsx | 2 ++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index b23d024a724dd1..0638842814a7d3 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -13,6 +13,9 @@ - `ToggleGroupControl`: Fix active background for `0` value ([#66855](https://github.com/WordPress/gutenberg/pull/66855)). - `SlotFill`: Fix a bug where a stale value of `fillProps` could be used ([#67000](https://github.com/WordPress/gutenberg/pull/67000)). +- `ColorPalette`: Disable `Clear` button if there's no color value. ([#67108](https://github.com/WordPress/gutenberg/pull/67108)). +- `GradientPicker`: Disable `Clear` button if there's no value. ([#67108](https://github.com/WordPress/gutenberg/pull/67108)). +- `DuotonePicker`: Disable `Clear` button if there's no value. ([#67108](https://github.com/WordPress/gutenberg/pull/67108)). - `ColorPicker`: Add accessible label for copy button ([#67094](https://github.com/WordPress/gutenberg/pull/67094)). ### Enhancements diff --git a/packages/components/src/color-palette/index.tsx b/packages/components/src/color-palette/index.tsx index a65508d8278c5f..ed3e9039704179 100644 --- a/packages/components/src/color-palette/index.tsx +++ b/packages/components/src/color-palette/index.tsx @@ -249,7 +249,11 @@ function UnforwardedColorPalette( }; const actions = !! clearable && ( - + { __( 'Clear' ) } ); diff --git a/packages/components/src/duotone-picker/duotone-picker.tsx b/packages/components/src/duotone-picker/duotone-picker.tsx index ee54c9cdf4235e..8764b401c38296 100644 --- a/packages/components/src/duotone-picker/duotone-picker.tsx +++ b/packages/components/src/duotone-picker/duotone-picker.tsx @@ -168,6 +168,8 @@ function DuotonePicker( { !! clearable && ( onChange( undefined ) } + accessibleWhenDisabled + disabled={ ! value } > { __( 'Clear' ) } diff --git a/packages/components/src/gradient-picker/index.tsx b/packages/components/src/gradient-picker/index.tsx index 8368279b8afd70..f0607badd1b03a 100644 --- a/packages/components/src/gradient-picker/index.tsx +++ b/packages/components/src/gradient-picker/index.tsx @@ -247,6 +247,8 @@ export function GradientPicker( { ! disableCustomGradients && ( { __( 'Clear' ) } From 110471ff548c053750553dd94cf2677f78688ade Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Wed, 20 Nov 2024 15:15:38 +0400 Subject: [PATCH 18/31] Global Styles: Don't call store actions during the render (#67146) Co-authored-by: Mamaduka Co-authored-by: getdave --- .../src/components/global-styles/screen-style-variations.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/edit-site/src/components/global-styles/screen-style-variations.js b/packages/edit-site/src/components/global-styles/screen-style-variations.js index 12690966ba3c4f..76d13023ff09a8 100644 --- a/packages/edit-site/src/components/global-styles/screen-style-variations.js +++ b/packages/edit-site/src/components/global-styles/screen-style-variations.js @@ -6,6 +6,7 @@ import { __ } from '@wordpress/i18n'; import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; import { useDispatch } from '@wordpress/data'; import { store as editorStore } from '@wordpress/editor'; +import { useEffect } from '@wordpress/element'; /** * Internal dependencies @@ -22,7 +23,9 @@ function ScreenStyleVariations() { // - "Desktop" device preview const { setDeviceType } = useDispatch( editorStore ); useZoomOut(); - setDeviceType( 'desktop' ); + useEffect( () => { + setDeviceType( 'desktop' ); + }, [ setDeviceType ] ); return ( <> From 58e2d4b7ef9df24c537b1a2cf9485c2b2849822d Mon Sep 17 00:00:00 2001 From: Bernie Reiter <96308+ockham@users.noreply.github.com> Date: Wed, 20 Nov 2024 12:52:51 +0100 Subject: [PATCH 19/31] Plugin: Bump minimum required WordPress version to 6.6 (#67117) And remove WP 6.6 compat layer. --- .github/workflows/performance.yml | 6 +- gutenberg.php | 4 +- lib/block-template-utils.php | 4 +- ...class-wp-theme-json-resolver-gutenberg.php | 2 +- lib/compat/wordpress-6.6/admin-bar.php | 48 - .../block-bindings/pattern-overrides.php | 62 - lib/compat/wordpress-6.6/block-editor.php | 50 - .../wordpress-6.6/block-template-utils.php | 362 -- lib/compat/wordpress-6.6/blocks.php | 46 - ...global-styles-revisions-controller-6-6.php | 97 - ...utenberg-rest-templates-controller-6-6.php | 131 - .../class-gutenberg-token-map-6-6.php | 818 ---- lib/compat/wordpress-6.6/compat.php | 32 - .../class-gutenberg-html-decoder-6-6.php | 463 --- ...class-gutenberg-html-open-elements-6-6.php | 541 --- .../class-gutenberg-html-processor-6-6.php | 2472 ------------ ...ass-gutenberg-html-processor-state-6-6.php | 143 - .../class-gutenberg-html-stack-event-6-6.php | 82 - ...class-gutenberg-html-tag-processor-6-6.php | 3559 ----------------- ...g-html5-named-character-references-6-6.php | 1315 ------ lib/compat/wordpress-6.6/option.php | 53 - lib/compat/wordpress-6.6/post.php | 39 - lib/compat/wordpress-6.6/resolve-patterns.php | 84 - lib/compat/wordpress-6.6/rest-api.php | 216 - ...utenberg-rest-templates-controller-6-7.php | 2 +- lib/compat/wordpress-6.7/compat.php | 2 +- lib/load.php | 27 +- lib/rest-api.php | 9 +- phpunit/blocks/get-block-templates-test.php | 109 - phpunit/class-wp-theme-json-test.php | 6 +- readme.txt | 2 +- 31 files changed, 19 insertions(+), 10767 deletions(-) delete mode 100644 lib/compat/wordpress-6.6/admin-bar.php delete mode 100644 lib/compat/wordpress-6.6/block-bindings/pattern-overrides.php delete mode 100644 lib/compat/wordpress-6.6/block-editor.php delete mode 100644 lib/compat/wordpress-6.6/block-template-utils.php delete mode 100644 lib/compat/wordpress-6.6/blocks.php delete mode 100644 lib/compat/wordpress-6.6/class-gutenberg-rest-global-styles-revisions-controller-6-6.php delete mode 100644 lib/compat/wordpress-6.6/class-gutenberg-rest-templates-controller-6-6.php delete mode 100644 lib/compat/wordpress-6.6/class-gutenberg-token-map-6-6.php delete mode 100644 lib/compat/wordpress-6.6/compat.php delete mode 100644 lib/compat/wordpress-6.6/html-api/class-gutenberg-html-decoder-6-6.php delete mode 100644 lib/compat/wordpress-6.6/html-api/class-gutenberg-html-open-elements-6-6.php delete mode 100644 lib/compat/wordpress-6.6/html-api/class-gutenberg-html-processor-6-6.php delete mode 100644 lib/compat/wordpress-6.6/html-api/class-gutenberg-html-processor-state-6-6.php delete mode 100644 lib/compat/wordpress-6.6/html-api/class-gutenberg-html-stack-event-6-6.php delete mode 100644 lib/compat/wordpress-6.6/html-api/class-gutenberg-html-tag-processor-6-6.php delete mode 100644 lib/compat/wordpress-6.6/html-api/gutenberg-html5-named-character-references-6-6.php delete mode 100644 lib/compat/wordpress-6.6/option.php delete mode 100644 lib/compat/wordpress-6.6/post.php delete mode 100644 lib/compat/wordpress-6.6/resolve-patterns.php delete mode 100644 lib/compat/wordpress-6.6/rest-api.php delete mode 100644 phpunit/blocks/get-block-templates-test.php diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 6c8c984602edcb..4a5b576b424b53 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -69,13 +69,13 @@ jobs: - name: Compare performance with base branch if: github.event_name == 'push' # The base hash used here need to be a commit that is compatible with the current WP version - # The current one is 5f4c9c853b15092ed885d5280edefb973c37d9e9 and it needs to be updated every WP major release. + # The current one is c7722262e65a3f4d0f1a2d1ad29eccb2069509e4 and it needs to be updated every WP major release. # It is used as a base comparison point to avoid fluctuation in the performance metrics. run: | WP_VERSION=$(awk -F ': ' '/^Tested up to/{print $2}' readme.txt) IFS=. read -ra WP_VERSION_ARRAY <<< "$WP_VERSION" WP_MAJOR="${WP_VERSION_ARRAY[0]}.${WP_VERSION_ARRAY[1]}" - ./bin/plugin/cli.js perf $GITHUB_SHA 5f4c9c853b15092ed885d5280edefb973c37d9e9 --tests-branch $GITHUB_SHA --wp-version "$WP_MAJOR" + ./bin/plugin/cli.js perf $GITHUB_SHA c7722262e65a3f4d0f1a2d1ad29eccb2069509e4 --tests-branch $GITHUB_SHA --wp-version "$WP_MAJOR" - name: Compare performance with custom branches if: github.event_name == 'workflow_dispatch' @@ -101,7 +101,7 @@ jobs: CODEHEALTH_PROJECT_TOKEN: ${{ secrets.CODEHEALTH_PROJECT_TOKEN }} run: | COMMITTED_AT=$(git show -s $GITHUB_SHA --format="%cI") - ./bin/log-performance-results.js $CODEHEALTH_PROJECT_TOKEN trunk $GITHUB_SHA 5f4c9c853b15092ed885d5280edefb973c37d9e9 $COMMITTED_AT + ./bin/log-performance-results.js $CODEHEALTH_PROJECT_TOKEN trunk $GITHUB_SHA c7722262e65a3f4d0f1a2d1ad29eccb2069509e4 $COMMITTED_AT - name: Archive debug artifacts (screenshots, HTML snapshots) uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 diff --git a/gutenberg.php b/gutenberg.php index f79182f3ead567..738fe1a3bd5268 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -3,7 +3,7 @@ * Plugin Name: Gutenberg * Plugin URI: https://github.com/WordPress/gutenberg * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. - * Requires at least: 6.5 + * Requires at least: 6.6 * Requires PHP: 7.2 * Version: 19.7.0-rc.2 * Author: Gutenberg Team @@ -15,7 +15,7 @@ ### BEGIN AUTO-GENERATED DEFINES defined( 'GUTENBERG_DEVELOPMENT_MODE' ) or define( 'GUTENBERG_DEVELOPMENT_MODE', true ); ### END AUTO-GENERATED DEFINES -defined( 'GUTENBERG_MINIMUM_WP_VERSION' ) or define( 'GUTENBERG_MINIMUM_WP_VERSION', '6.5' ); +defined( 'GUTENBERG_MINIMUM_WP_VERSION' ) or define( 'GUTENBERG_MINIMUM_WP_VERSION', '6.6' ); gutenberg_pre_init(); diff --git a/lib/block-template-utils.php b/lib/block-template-utils.php index a644047d3cfdc1..7dba2ff518104d 100644 --- a/lib/block-template-utils.php +++ b/lib/block-template-utils.php @@ -60,7 +60,7 @@ function gutenberg_generate_block_templates_export_file() { } // Load templates into the zip file. - $templates = gutenberg_get_block_templates(); + $templates = get_block_templates(); foreach ( $templates as $template ) { $template->content = traverse_and_serialize_blocks( parse_blocks( $template->content ), @@ -74,7 +74,7 @@ function gutenberg_generate_block_templates_export_file() { } // Load template parts into the zip file. - $template_parts = gutenberg_get_block_templates( array(), 'wp_template_part' ); + $template_parts = get_block_templates( array(), 'wp_template_part' ); foreach ( $template_parts as $template_part ) { $zip->addFromString( 'parts/' . $template_part->slug . '.html', diff --git a/lib/class-wp-theme-json-resolver-gutenberg.php b/lib/class-wp-theme-json-resolver-gutenberg.php index cd02b5a45c22f7..1f45d897a77cc0 100644 --- a/lib/class-wp-theme-json-resolver-gutenberg.php +++ b/lib/class-wp-theme-json-resolver-gutenberg.php @@ -316,7 +316,7 @@ public static function get_theme_data( $deprecated = array(), $options = array() * So we take theme supports, transform it to theme.json shape * and merge the static::$theme upon that. */ - $theme_support_data = WP_Theme_JSON_Gutenberg::get_from_editor_settings( gutenberg_get_classic_theme_supports_block_editor_settings() ); + $theme_support_data = WP_Theme_JSON_Gutenberg::get_from_editor_settings( get_classic_theme_supports_block_editor_settings() ); if ( ! wp_theme_has_theme_json() ) { /* * Unlike block themes, classic themes without a theme.json disable diff --git a/lib/compat/wordpress-6.6/admin-bar.php b/lib/compat/wordpress-6.6/admin-bar.php deleted file mode 100644 index b7a77faebea72a..00000000000000 --- a/lib/compat/wordpress-6.6/admin-bar.php +++ /dev/null @@ -1,48 +0,0 @@ -add_node( - array( - 'id' => 'site-editor', - 'title' => __( 'Edit site' ), - 'href' => add_query_arg( - array( - 'postType' => 'wp_template', - 'postId' => $_wp_current_template_id, - 'canvas' => 'edit', - ), - admin_url( 'site-editor.php' ) - ), - ) - ); -} -remove_action( 'admin_bar_menu', 'wp_admin_bar_edit_site_menu', 40 ); -add_action( 'admin_bar_menu', 'gutenberg_admin_bar_edit_site_menu', 41 ); diff --git a/lib/compat/wordpress-6.6/block-bindings/pattern-overrides.php b/lib/compat/wordpress-6.6/block-bindings/pattern-overrides.php deleted file mode 100644 index e5f9891f04c471..00000000000000 --- a/lib/compat/wordpress-6.6/block-bindings/pattern-overrides.php +++ /dev/null @@ -1,62 +0,0 @@ - "foo" ). - * @param WP_Block $block_instance The block instance. - * @param string $attribute_name The name of the target attribute. - * @return mixed The value computed for the source. - */ -function gutenberg_block_bindings_pattern_overrides_callback( $source_attrs, $block_instance, $attribute_name ) { - if ( ! isset( $block_instance->context['pattern/overrides'] ) ) { - return null; - } - - $override_content = $block_instance->context['pattern/overrides']; - - // Back compat. Pattern overrides previously used a metadata `id` instead of `name`. - // We check first for the name, and if it exists, use that value. - if ( isset( $block_instance->attributes['metadata']['name'] ) ) { - $metadata_name = $block_instance->attributes['metadata']['name']; - if ( array_key_exists( $metadata_name, $override_content ) ) { - return _wp_array_get( $override_content, array( $metadata_name, $attribute_name ), null ); - } - } - - // Next check for the `id`. - if ( isset( $block_instance->attributes['metadata']['id'] ) ) { - $metadata_id = $block_instance->attributes['metadata']['id']; - if ( array_key_exists( $metadata_id, $override_content ) ) { - return _wp_array_get( $override_content, array( $metadata_id, $attribute_name ), null ); - } - } - - return null; -} - -/** - * Registers Pattern Overrides source in the Block Bindings registry. - */ -function gutenberg_register_block_bindings_pattern_overrides_source() { - // Override the "core/pattern-overrides" source from core. - if ( array_key_exists( 'core/pattern-overrides', get_all_registered_block_bindings_sources() ) ) { - unregister_block_bindings_source( 'core/pattern-overrides' ); - } - register_block_bindings_source( - 'core/pattern-overrides', - array( - 'label' => _x( 'Pattern Overrides', 'block bindings source' ), - 'get_value_callback' => 'gutenberg_block_bindings_pattern_overrides_callback', - 'uses_context' => array( 'pattern/overrides' ), - ) - ); -} - -add_action( 'init', 'gutenberg_register_block_bindings_pattern_overrides_source' ); diff --git a/lib/compat/wordpress-6.6/block-editor.php b/lib/compat/wordpress-6.6/block-editor.php deleted file mode 100644 index 6253f6a0adca4f..00000000000000 --- a/lib/compat/wordpress-6.6/block-editor.php +++ /dev/null @@ -1,50 +0,0 @@ - get_theme_support( 'disable-custom-colors' ), - 'disableCustomFontSizes' => get_theme_support( 'disable-custom-font-sizes' ), - 'disableCustomGradients' => get_theme_support( 'disable-custom-gradients' ), - 'disableLayoutStyles' => get_theme_support( 'disable-layout-styles' ), - 'enableCustomLineHeight' => get_theme_support( 'custom-line-height' ), - 'enableCustomSpacing' => get_theme_support( 'custom-spacing' ), - 'enableCustomUnits' => get_theme_support( 'custom-units' ), - ); - - // Theme settings. - $color_palette = current( (array) get_theme_support( 'editor-color-palette' ) ); - if ( false !== $color_palette ) { - $theme_settings['colors'] = $color_palette; - } - - $font_sizes = current( (array) get_theme_support( 'editor-font-sizes' ) ); - if ( false !== $font_sizes ) { - $theme_settings['fontSizes'] = $font_sizes; - } - - $gradient_presets = current( (array) get_theme_support( 'editor-gradient-presets' ) ); - if ( false !== $gradient_presets ) { - $theme_settings['gradients'] = $gradient_presets; - } - - $spacing_sizes = current( (array) get_theme_support( 'editor-spacing-sizes' ) ); - if ( false !== $spacing_sizes ) { - $theme_settings['spacingSizes'] = $spacing_sizes; - } - - return $theme_settings; -} diff --git a/lib/compat/wordpress-6.6/block-template-utils.php b/lib/compat/wordpress-6.6/block-template-utils.php deleted file mode 100644 index 953f6bf20c077e..00000000000000 --- a/lib/compat/wordpress-6.6/block-template-utils.php +++ /dev/null @@ -1,362 +0,0 @@ - strlen( $item ) + 1 ) { - $template_hierarchy[] = "$type-$item"; - $template_hierarchy[] = $type; - break; - } - } - } - // Handle `archive` template. - if ( - str_starts_with( $slug, 'author' ) || - str_starts_with( $slug, 'taxonomy' ) || - str_starts_with( $slug, 'category' ) || - str_starts_with( $slug, 'tag' ) || - 'date' === $slug - ) { - $template_hierarchy[] = 'archive'; - } - // Handle `single` template. - if ( 'attachment' === $slug ) { - $template_hierarchy[] = 'single'; - } - // Handle `singular` template. - if ( - str_starts_with( $slug, 'single' ) || - str_starts_with( $slug, 'page' ) || - 'attachment' === $slug - ) { - $template_hierarchy[] = 'singular'; - } - $template_hierarchy[] = 'index'; - - $template_type = ''; - if ( ! empty( $template_prefix ) ) { - list( $template_type ) = explode( '-', $template_prefix ); - } else { - list( $template_type ) = explode( '-', $slug ); - } - $valid_template_types = array( '404', 'archive', 'attachment', 'author', 'category', 'date', 'embed', 'frontpage', 'home', 'index', 'page', 'paged', 'privacypolicy', 'search', 'single', 'singular', 'tag', 'taxonomy' ); - if ( in_array( $template_type, $valid_template_types, true ) ) { - /** This filter is documented in wp-includes/template.php */ - return apply_filters( "{$template_type}_template_hierarchy", $template_hierarchy ); - } - return $template_hierarchy; -} - -/** - * Retrieves the template files from the theme. - * - * @since 5.9.0 - * @since 6.3.0 Added the `$query` parameter. - * @access private - * - * @param string $template_type Template type. Either 'wp_template' or 'wp_template_part'. - * @param array $query { - * Arguments to retrieve templates. Optional, empty by default. - * - * @type string[] $slug__in List of slugs to include. - * @type string[] $slug__not_in List of slugs to skip. - * @type string $area A 'wp_template_part_area' taxonomy value to filter by (for 'wp_template_part' template type only). - * @type string $post_type Post type to get the templates for. - * } - * - * @return array Template - */ -function _gutenberg_get_block_templates_files( $template_type, $query = array() ) { - if ( 'wp_template' !== $template_type && 'wp_template_part' !== $template_type ) { - return null; - } - - // @core-merge: This code will go into Core's '_get_block_templates_files' function. - $default_template_types = array(); - if ( 'wp_template' === $template_type ) { - $default_template_types = get_default_block_template_types(); - } - // @core-merge: End of the code that will go into Core. - - // Prepare metadata from $query. - $slugs_to_include = isset( $query['slug__in'] ) ? $query['slug__in'] : array(); - $slugs_to_skip = isset( $query['slug__not_in'] ) ? $query['slug__not_in'] : array(); - $area = isset( $query['area'] ) ? $query['area'] : null; - $post_type = isset( $query['post_type'] ) ? $query['post_type'] : ''; - - $stylesheet = get_stylesheet(); - $template = get_template(); - $themes = array( - $stylesheet => get_stylesheet_directory(), - ); - // Add the parent theme if it's not the same as the current theme. - if ( $stylesheet !== $template ) { - $themes[ $template ] = get_template_directory(); - } - $template_files = array(); - foreach ( $themes as $theme_slug => $theme_dir ) { - $template_base_paths = get_block_theme_folders( $theme_slug ); - $theme_template_files = _get_block_templates_paths( $theme_dir . '/' . $template_base_paths[ $template_type ] ); - foreach ( $theme_template_files as $template_file ) { - $template_base_path = $template_base_paths[ $template_type ]; - $template_slug = substr( - $template_file, - // Starting position of slug. - strpos( $template_file, $template_base_path . DIRECTORY_SEPARATOR ) + 1 + strlen( $template_base_path ), - // Subtract ending '.html'. - -5 - ); - - // Skip this item if its slug doesn't match any of the slugs to include. - if ( ! empty( $slugs_to_include ) && ! in_array( $template_slug, $slugs_to_include, true ) ) { - continue; - } - - // Skip this item if its slug matches any of the slugs to skip. - if ( ! empty( $slugs_to_skip ) && in_array( $template_slug, $slugs_to_skip, true ) ) { - continue; - } - - /* - * The child theme items (stylesheet) are processed before the parent theme's (template). - * If a child theme defines a template, prevent the parent template from being added to the list as well. - */ - if ( isset( $template_files[ $template_slug ] ) ) { - continue; - } - - $new_template_item = array( - 'slug' => $template_slug, - 'path' => $template_file, - 'theme' => $theme_slug, - 'type' => $template_type, - ); - - if ( 'wp_template_part' === $template_type ) { - $candidate = _add_block_template_part_area_info( $new_template_item ); - if ( ! isset( $area ) || ( isset( $area ) && $area === $candidate['area'] ) ) { - $template_files[ $template_slug ] = $candidate; - } - } - - if ( 'wp_template' === $template_type ) { - $candidate = _add_block_template_info( $new_template_item ); - $is_custom = ! isset( $default_template_types[ $candidate['slug'] ] ); - - if ( - ! $post_type || - ( $post_type && isset( $candidate['postTypes'] ) && in_array( $post_type, $candidate['postTypes'], true ) ) - ) { - $template_files[ $template_slug ] = $candidate; - } - - // @core-merge: This code will go into Core's '_get_block_templates_files' function. - // The custom templates with no associated post-types are available for all post-types. - if ( $post_type && ! isset( $candidate['postTypes'] ) && $is_custom ) { - $template_files[ $template_slug ] = $candidate; - } - // @core-merge: End of the code that will go into Core. - } - } - } - - return array_values( $template_files ); -} - -/** - * Retrieves a list of unified template objects based on a query. - * - * @since 5.8.0 - * - * @param array $query { - * Optional. Arguments to retrieve templates. - * - * @type string[] $slug__in List of slugs to include. - * @type int $wp_id Post ID of customized template. - * @type string $area A 'wp_template_part_area' taxonomy value to filter by (for 'wp_template_part' template type only). - * @type string $post_type Post type to get the templates for. - * } - * @param string $template_type Template type. Either 'wp_template' or 'wp_template_part'. - * @return WP_Block_Template[] Array of block templates. - */ -function gutenberg_get_block_templates( $query = array(), $template_type = 'wp_template' ) { - /** - * Filters the block templates array before the query takes place. - * - * Return a non-null value to bypass the WordPress queries. - * - * @since 5.9.0 - * - * @param WP_Block_Template[]|null $block_templates Return an array of block templates to short-circuit the default query, - * or null to allow WP to run its normal queries. - * @param array $query { - * Arguments to retrieve templates. All arguments are optional. - * - * @type string[] $slug__in List of slugs to include. - * @type int $wp_id Post ID of customized template. - * @type string $area A 'wp_template_part_area' taxonomy value to filter by (for 'wp_template_part' template type only). - * @type string $post_type Post type to get the templates for. - * } - * @param string $template_type Template type. Either 'wp_template' or 'wp_template_part'. - */ - $templates = apply_filters( 'pre_get_block_templates', null, $query, $template_type ); - if ( ! is_null( $templates ) ) { - return $templates; - } - - $post_type = isset( $query['post_type'] ) ? $query['post_type'] : ''; - $wp_query_args = array( - 'post_status' => array( 'auto-draft', 'draft', 'publish' ), - 'post_type' => $template_type, - 'posts_per_page' => -1, - 'no_found_rows' => true, - 'lazy_load_term_meta' => false, - 'tax_query' => array( - array( - 'taxonomy' => 'wp_theme', - 'field' => 'name', - 'terms' => get_stylesheet(), - ), - ), - ); - - if ( 'wp_template_part' === $template_type && isset( $query['area'] ) ) { - $wp_query_args['tax_query'][] = array( - 'taxonomy' => 'wp_template_part_area', - 'field' => 'name', - 'terms' => $query['area'], - ); - $wp_query_args['tax_query']['relation'] = 'AND'; - } - - if ( ! empty( $query['slug__in'] ) ) { - $wp_query_args['post_name__in'] = $query['slug__in']; - $wp_query_args['posts_per_page'] = count( array_unique( $query['slug__in'] ) ); - } - - // This is only needed for the regular templates/template parts post type listing and editor. - if ( isset( $query['wp_id'] ) ) { - $wp_query_args['p'] = $query['wp_id']; - } else { - $wp_query_args['post_status'] = 'publish'; - } - - $template_query = new WP_Query( $wp_query_args ); - $query_result = array(); - foreach ( $template_query->posts as $post ) { - $template = _build_block_template_result_from_post( $post ); - - if ( is_wp_error( $template ) ) { - continue; - } - - if ( $post_type && ! $template->is_custom ) { - continue; - } - - if ( - $post_type && - isset( $template->post_types ) && - ! in_array( $post_type, $template->post_types, true ) - ) { - continue; - } - - $query_result[] = $template; - } - - if ( ! isset( $query['wp_id'] ) ) { - /* - * If the query has found some use templates, those have priority - * over the theme-provided ones, so we skip querying and building them. - */ - $query['slug__not_in'] = wp_list_pluck( $query_result, 'slug' ); - $template_files = _gutenberg_get_block_templates_files( $template_type, $query ); - foreach ( $template_files as $template_file ) { - $query_result[] = _build_block_template_result_from_file( $template_file, $template_type ); - } - } - - /** - * Filters the array of queried block templates array after they've been fetched. - * - * @since 5.9.0 - * - * @param WP_Block_Template[] $query_result Array of found block templates. - * @param array $query { - * Arguments to retrieve templates. All arguments are optional. - * - * @type string[] $slug__in List of slugs to include. - * @type int $wp_id Post ID of customized template. - * @type string $area A 'wp_template_part_area' taxonomy value to filter by (for 'wp_template_part' template type only). - * @type string $post_type Post type to get the templates for. - * } - * @param string $template_type wp_template or wp_template_part. - */ - return apply_filters( 'get_block_templates', $query_result, $query, $template_type ); -} diff --git a/lib/compat/wordpress-6.6/blocks.php b/lib/compat/wordpress-6.6/blocks.php deleted file mode 100644 index 0d8805a489d9cb..00000000000000 --- a/lib/compat/wordpress-6.6/blocks.php +++ /dev/null @@ -1,46 +0,0 @@ - array( 'content' ), - 'core/heading' => array( 'content' ), - 'core/image' => array( 'id', 'url', 'title', 'alt' ), - 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ), - ); - - $bindings = $parsed_block['attrs']['metadata']['bindings'] ?? array(); - if ( - isset( $bindings['__default']['source'] ) && - 'core/pattern-overrides' === $bindings['__default']['source'] - ) { - $updated_bindings = array(); - - // Build an binding array of all supported attributes. - // Note that this also omits the `__default` attribute from the - // resulting array. - foreach ( $supported_block_attrs[ $parsed_block['blockName'] ] as $attribute_name ) { - // Retain any non-pattern override bindings that might be present. - $updated_bindings[ $attribute_name ] = isset( $bindings[ $attribute_name ] ) - ? $bindings[ $attribute_name ] - : array( 'source' => 'core/pattern-overrides' ); - } - $parsed_block['attrs']['metadata']['bindings'] = $updated_bindings; - } - - return $parsed_block; -} - -add_filter( 'render_block_data', 'gutenberg_replace_pattern_override_default_binding', 10, 1 ); diff --git a/lib/compat/wordpress-6.6/class-gutenberg-rest-global-styles-revisions-controller-6-6.php b/lib/compat/wordpress-6.6/class-gutenberg-rest-global-styles-revisions-controller-6-6.php deleted file mode 100644 index 3e5d4cdd68454a..00000000000000 --- a/lib/compat/wordpress-6.6/class-gutenberg-rest-global-styles-revisions-controller-6-6.php +++ /dev/null @@ -1,97 +0,0 @@ -get_parent( $request['parent'] ); - $global_styles_config = $this->get_decoded_global_styles_json( $post->post_content ); - - if ( is_wp_error( $global_styles_config ) ) { - return $global_styles_config; - } - - $fields = $this->get_fields_for_response( $request ); - $data = array(); - $theme_json = array(); - - if ( ! empty( $global_styles_config['styles'] ) || ! empty( $global_styles_config['settings'] ) ) { - $theme_json = new WP_Theme_JSON_Gutenberg( $global_styles_config, 'custom' ); - $global_styles_config = ( $theme_json )->get_raw_data(); - - if ( rest_is_field_included( 'settings', $fields ) ) { - $data['settings'] = ! empty( $global_styles_config['settings'] ) ? $global_styles_config['settings'] : new stdClass(); - } - if ( rest_is_field_included( 'styles', $fields ) ) { - $data['styles'] = ! empty( $global_styles_config['styles'] ) ? $global_styles_config['styles'] : new stdClass(); - } - } - - if ( rest_is_field_included( 'author', $fields ) ) { - $data['author'] = (int) $post->post_author; - } - - if ( rest_is_field_included( 'date', $fields ) ) { - $data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date ); - } - - if ( rest_is_field_included( 'date_gmt', $fields ) ) { - $data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt ); - } - - if ( rest_is_field_included( 'id', $fields ) ) { - $data['id'] = (int) $post->ID; - } - - if ( rest_is_field_included( 'modified', $fields ) ) { - $data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified ); - } - - if ( rest_is_field_included( 'modified_gmt', $fields ) ) { - $data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt ); - } - - if ( rest_is_field_included( 'parent', $fields ) ) { - $data['parent'] = (int) $parent->ID; - } - - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $context ); - - $response = rest_ensure_response( $data ); - - // Add resolved URIs to the response. - $links = array(); - $resolved_theme_uris = WP_Theme_JSON_Resolver_Gutenberg::get_resolved_theme_uris( $theme_json ); - if ( ! empty( $resolved_theme_uris ) ) { - $links['https://api.w.org/theme-file'] = $resolved_theme_uris; - } - $response->add_links( $links ); - - return $response; - } -} diff --git a/lib/compat/wordpress-6.6/class-gutenberg-rest-templates-controller-6-6.php b/lib/compat/wordpress-6.6/class-gutenberg-rest-templates-controller-6-6.php deleted file mode 100644 index 034187ca9a70ae..00000000000000 --- a/lib/compat/wordpress-6.6/class-gutenberg-rest-templates-controller-6-6.php +++ /dev/null @@ -1,131 +0,0 @@ - true ), 'objects' ) as $post_type ) { - if ( current_user_can( $post_type->cap->edit_posts ) ) { - return true; - } - } - - return new WP_Error( - 'rest_cannot_manage_templates', - __( 'Sorry, you are not allowed to access the templates on this site.', 'default' ), - array( - 'status' => rest_authorization_required_code(), - ) - ); - } - - /** - * Returns a list of templates. - * - * @since 5.8.0 - * - * @param WP_REST_Request $request The request instance. - * @return WP_REST_Response - */ - public function get_items( $request ) { - $query = array(); - if ( isset( $request['wp_id'] ) ) { - $query['wp_id'] = $request['wp_id']; - } - if ( isset( $request['area'] ) ) { - $query['area'] = $request['area']; - } - if ( isset( $request['post_type'] ) ) { - $query['post_type'] = $request['post_type']; - } - - $templates = array(); - foreach ( gutenberg_get_block_templates( $query, $this->post_type ) as $template ) { - $data = $this->prepare_item_for_response( $template, $request ); - $templates[] = $this->prepare_response_for_collection( $data ); - } - - return rest_ensure_response( $templates ); - } - - /** - * Checks if a given request has access to read templates. - * - * @since 6.6.0 - * - * @param WP_REST_Request $request Full details about the request. - * @return true|WP_Error True if the request has read access, WP_Error object otherwise. - */ - public function get_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - if ( current_user_can( 'edit_posts' ) ) { - return true; - } - foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) { - if ( current_user_can( $post_type->cap->edit_posts ) ) { - return true; - } - } - - return new WP_Error( - 'rest_cannot_manage_templates', - __( 'Sorry, you are not allowed to access the templates on this site.', 'default' ), - array( - 'status' => rest_authorization_required_code(), - ) - ); - } - - /** - * Returns the fallback template for the given slug. - * - * @since 6.1.0 - * - * @param WP_REST_Request $request The request instance. - * @return WP_REST_Response|WP_Error - */ - public function get_template_fallback( $request ) { - $hierarchy = gutenberg_get_template_hierarchy( $request['slug'], $request['is_custom'], $request['template_prefix'] ); - - do { - $fallback_template = resolve_block_template( $request['slug'], $hierarchy, '' ); - array_shift( $hierarchy ); - } while ( ! empty( $hierarchy ) && empty( $fallback_template->content ) ); - - // To maintain original behavior, return an empty object rather than a 404 error when no template is found. - $response = $fallback_template ? $this->prepare_item_for_response( $fallback_template, $request ) : new stdClass(); - - return rest_ensure_response( $response ); - } - - /** - * See WP_REST_Templates_Controller::prepare_item_for_response - */ - public function prepare_item_for_response( $item, $request ) { - $blocks = parse_blocks( $item->content ); - $blocks = gutenberg_replace_pattern_blocks( $blocks ); - $item->content = serialize_blocks( $blocks ); - return parent::prepare_item_for_response( $item, $request ); - } -} diff --git a/lib/compat/wordpress-6.6/class-gutenberg-token-map-6-6.php b/lib/compat/wordpress-6.6/class-gutenberg-token-map-6-6.php deleted file mode 100644 index 1df1fe91dab141..00000000000000 --- a/lib/compat/wordpress-6.6/class-gutenberg-token-map-6-6.php +++ /dev/null @@ -1,818 +0,0 @@ - '😯', - * ':(' => '🙁', - * ':)' => '🙂', - * ':?' => '😕', - * ) ); - * - * 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_6 { - /** - * 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( $mappings, $key_length = 2 ) { - $map = new static(); - $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, 'static::longest_first_then_alphabetical' ); - foreach ( $groups as $group_key => $group ) { - usort( - $groups[ $group_key ], - static function ( $a, $b ) { - 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 ) { - $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 static(); - - $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( $word, $case_sensitivity = 'case-sensitive' ) { - $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 &$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( $text, $offset = 0, &$matched_token_byte_length = null, $case_sensitivity = 'case-sensitive' ) { - $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 &$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( $text, $offset, &$matched_token_byte_length, $case_sensitivity = 'case-sensitive' ) { - $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() { - $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( $indent = "\t" ) { - $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( $a, $b ) { - 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.6/compat.php b/lib/compat/wordpress-6.6/compat.php deleted file mode 100644 index 4e444d3149824a..00000000000000 --- a/lib/compat/wordpress-6.6/compat.php +++ /dev/null @@ -1,32 +0,0 @@ -= 6.6.0. - * - * @global array $submenu - */ -function gutenberg_change_patterns_link_and_remove_template_parts_submenu_item() { - if ( ! wp_is_block_theme() ) { - global $submenu; - - if ( empty( $submenu['themes.php'] ) ) { - return; - } - - foreach ( $submenu['themes.php'] as $key => $item ) { - if ( 'edit.php?post_type=wp_block' === $item[2] ) { - $submenu['themes.php'][ $key ][2] = 'site-editor.php?path=/patterns'; - } elseif ( 'site-editor.php?path=/wp_template_part/all' === $item[2] ) { - unset( $submenu['themes.php'][ $key ] ); - } - } - } -} -add_action( 'admin_init', 'gutenberg_change_patterns_link_and_remove_template_parts_submenu_item' ); diff --git a/lib/compat/wordpress-6.6/html-api/class-gutenberg-html-decoder-6-6.php b/lib/compat/wordpress-6.6/html-api/class-gutenberg-html-decoder-6-6.php deleted file mode 100644 index d0b9f18bf5b29b..00000000000000 --- a/lib/compat/wordpress-6.6/html-api/class-gutenberg-html-decoder-6-6.php +++ /dev/null @@ -1,463 +0,0 @@ -= $end ) { - break; - } - - $character_reference = self::read_character_reference( $context, $text, $next_character_reference_at, $token_length ); - if ( isset( $character_reference ) ) { - $at = $next_character_reference_at; - $decoded .= substr( $text, $was_at, $at - $was_at ); - $decoded .= $character_reference; - $at += $token_length; - $was_at = $at; - continue; - } - - ++$at; - } - - if ( 0 === $was_at ) { - return $text; - } - - if ( $was_at < $end ) { - $decoded .= substr( $text, $was_at, $end - $was_at ); - } - - return $decoded; - } - - /** - * Attempt to read a character reference at the given location in a given string, - * depending on the context in which it's found. - * - * If a character reference is found, this function will return the translated value - * that the reference maps to. It will then set `$match_byte_length` the - * number of bytes of input it read while consuming the character reference. This - * gives calling code the opportunity to advance its cursor when traversing a string - * and decoding. - * - * Example: - * - * null === WP_HTML_Decoder::read_character_reference( 'attribute', 'Ships…', 0 ); - * '…' === WP_HTML_Decoder::read_character_reference( 'attribute', 'Ships…', 5, $token_length ); - * 8 === $token_length; // `…` - * - * null === WP_HTML_Decoder::read_character_reference( 'attribute', '¬in', 0 ); - * '∉' === WP_HTML_Decoder::read_character_reference( 'attribute', '∉', 0, $token_length ); - * 7 === $token_length; // `∉` - * - * '¬' === WP_HTML_Decoder::read_character_reference( 'data', '¬in', 0, $token_length ); - * 4 === $token_length; // `¬` - * '∉' === WP_HTML_Decoder::read_character_reference( 'data', '∉', 0, $token_length ); - * 7 === $token_length; // `∉` - * - * @since 6.6.0 - * - * @global WP_Token_Map $html5_named_character_references - * - * @param string $context `attribute` for decoding attribute values, `data` otherwise. - * @param string $text Text document containing span of text to decode. - * @param int $at Optional. Byte offset into text where span begins, defaults to the beginning (0). - * @param int &$match_byte_length Optional. Set to byte-length of character reference if provided and if a match - * is found, otherwise not set. Default null. - * @return string|false Decoded character reference in UTF-8 if found, otherwise `false`. - */ - public static function read_character_reference( $context, $text, $at = 0, &$match_byte_length = null ) { - /** - * Mappings for HTML5 named character references. - * - * @var WP_Token_Map $html5_named_character_references - */ - global $html5_named_character_references; - - $length = strlen( $text ); - if ( $at + 1 >= $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 ) { - // 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 = ( $code_point >> 6 ) | 0xC0; - $byte2 = $code_point & 0x3F | 0x80; - - return pack( 'CC', $byte1, $byte2 ); - } - - if ( $code_point <= 0xFFFF ) { - $byte1 = ( $code_point >> 12 ) | 0xE0; - $byte2 = ( $code_point >> 6 ) & 0x3F | 0x80; - $byte3 = $code_point & 0x3F | 0x80; - - return pack( 'CCC', $byte1, $byte2, $byte3 ); - } - - // Any values above U+10FFFF are eliminated above in the pre-check. - $byte1 = ( $code_point >> 18 ) | 0xF0; - $byte2 = ( $code_point >> 12 ) & 0x3F | 0x80; - $byte3 = ( $code_point >> 6 ) & 0x3F | 0x80; - $byte4 = $code_point & 0x3F | 0x80; - - return pack( 'CCCC', $byte1, $byte2, $byte3, $byte4 ); - } -} diff --git a/lib/compat/wordpress-6.6/html-api/class-gutenberg-html-open-elements-6-6.php b/lib/compat/wordpress-6.6/html-api/class-gutenberg-html-open-elements-6-6.php deleted file mode 100644 index da237e02bc63ba..00000000000000 --- a/lib/compat/wordpress-6.6/html-api/class-gutenberg-html-open-elements-6-6.php +++ /dev/null @@ -1,541 +0,0 @@ - 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_6 { - /** - * 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 - */ - 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 - */ - 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 ) { - $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 ) { - $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( $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 open elements. - * - * @since 6.4.0 - * - * @return int How many node are in the stack of open elements. - */ - public function count() { - 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() { - $current_node = end( $this->stack ); - - return $current_node ? $current_node : null; - } - - /** - * Returns whether an element is in a specific scope. - * - * ## HTML Support - * - * This function skips checking for the termination list because there - * are no supported elements which appear in the termination list. - * - * @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( $tag_name, $termination_list ) { - 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( $tag_name ) { - 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( $tag_name ) { - 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( $tag_name ) { - 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( $tag_name ) { - throw new WP_HTML_Unsupported_Exception( '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. - * - * @since 6.4.0 - * - * @see https://html.spec.whatwg.org/#has-an-element-in-select-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_select_scope( $tag_name ) { - throw new WP_HTML_Unsupported_Exception( 'Cannot process elements depending on select scope.' ); - - return false; // The linter requires this unreachable code until the function is implemented and can return. - } - - /** - * 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() { - 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() { - $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( $tag_name ) { - 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( $stack_item ) { - $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( $token ) { - 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 $above_this_node Start traversing above this node, if provided and if the node exists. - */ - public function walk_up( $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( $item ) { - /* - * 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( $item ) { - /* - * 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.6/html-api/class-gutenberg-html-processor-6-6.php b/lib/compat/wordpress-6.6/html-api/class-gutenberg-html-processor-6-6.php deleted file mode 100644 index 77801535ff3683..00000000000000 --- a/lib/compat/wordpress-6.6/html-api/class-gutenberg-html-processor-6-6.php +++ /dev/null @@ -1,2472 +0,0 @@ -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, PROGRESS, SEARCH. - * - 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_6 extends Gutenberg_HTML_Tag_Processor_6_6 { - /** - * 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 = null; - - /** - * 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; - - /** - * 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 - */ - 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(); - - /** - * 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 - */ - private $current_element = null; - - /** - * Context node if created as a fragment parser. - * - * @var ?WP_HTML_Token - */ - 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_6::INSERTION_MODE_IN_BODY; - - // @todo Create "fake" bookmarks for non-existent but implied nodes. - $processor->bookmarks['root-node'] = new WP_HTML_Span( 0, 0 ); - $processor->bookmarks['context-node'] = new WP_HTML_Span( 0, 0 ); - - $processor->state->stack_of_open_elements->push( - new WP_HTML_Token( - 'root-node', - 'HTML', - false - ) - ); - - $context_node = new WP_HTML_Token( - 'context-node', - $processor->state->context_node[0], - false - ); - - $processor->state->stack_of_open_elements->push( $context_node ); - $processor->context_node = $context_node; - - 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_6(); - - $this->state->stack_of_open_elements->set_push_handler( - function ( WP_HTML_Token $token ) { - $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_6( $token, Gutenberg_HTML_Stack_Event_6_6::PUSH, $provenance ); - } - ); - - $this->state->stack_of_open_elements->set_pop_handler( - function ( WP_HTML_Token $token ) { - $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_6( $token, Gutenberg_HTML_Stack_Event_6_6::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 ( $name ) { - parent::release_bookmark( $name ); - }; - } - - /** - * 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( '