diff --git a/bin/api-docs/gen-theme-reference.js b/bin/api-docs/gen-theme-reference.js index f638bb708890a8..0ea9e282e5463e 100644 --- a/bin/api-docs/gen-theme-reference.js +++ b/bin/api-docs/gen-theme-reference.js @@ -74,6 +74,42 @@ const keys = ( maybeObject ) => { return Object.keys( maybeObject ); }; +/** + * Get definition from ref. + * + * @param {string} ref + * @return {Object} definition + * @throws {Error} If the referenced definition is not found in 'themejson.definitions'. + * + * @example + * getDefinition( '#/definitions/typographyProperties/properties/fontFamily' ) + * // returns themejson.definitions.typographyProperties.properties.fontFamily + */ +const resolveDefinitionRef = ( ref ) => { + const refParts = ref.split( '/' ); + const definition = refParts[ refParts.length - 1 ]; + if ( ! themejson.definitions[ definition ] ) { + throw new Error( `Can't resolve '${ ref }'. Definition not found` ); + } + return themejson.definitions[ definition ]; +}; + +/** + * Get properties from an array. + * + * @param {Object} items + * @return {Object} properties + */ +const getPropertiesFromArray = ( items ) => { + // if its a $ref resolve it + if ( items.$ref ) { + return resolveDefinitionRef( items.$ref ).properties; + } + + // otherwise just return the properties + return items.properties; +}; + /** * Convert settings properties to markup. * @@ -96,7 +132,9 @@ const getSettingsPropertiesMarkup = ( struct ) => { const def = 'default' in props[ key ] ? props[ key ].default : ''; const ps = props[ key ].type === 'array' - ? keys( props[ key ].items.properties ).sort().join( ', ' ) + ? keys( getPropertiesFromArray( props[ key ].items ) ) + .sort() + .join( ', ' ) : ''; markup += `| ${ key } | ${ props[ key ].type } | ${ def } | ${ ps } |\n`; } ); diff --git a/bin/cherry-pick.mjs b/bin/cherry-pick.mjs index 0c6a6c613638ba..dc71eed751cfdc 100644 --- a/bin/cherry-pick.mjs +++ b/bin/cherry-pick.mjs @@ -10,7 +10,7 @@ const LABEL = process.argv[ 2 ] || 'Backport to WP Beta/RC'; const BRANCH = getCurrentBranch(); const GITHUB_CLI_AVAILABLE = spawnSync( 'gh', [ 'auth', 'status' ] ) ?.stdout?.toString() - .includes( '✓ Logged in to github.com as' ); + .includes( '✓ Logged in to github.com' ); const AUTO_PROPAGATE_RESULTS_TO_GITHUB = GITHUB_CLI_AVAILABLE; @@ -114,16 +114,23 @@ async function fetchPRs() { const { items } = await GitHubFetch( `/search/issues?q=is:pr state:closed sort:updated label:"${ LABEL }" repo:WordPress/gutenberg` ); - const PRs = items.map( ( { id, number, title, pull_request, closed_at } ) => ( { - id, - number, - title, - pull_request, - } ) ) + const PRs = items + .map( ( { id, number, title, pull_request, closed_at } ) => ( { + id, + number, + title, + pull_request, + } ) ) .filter( ( { pull_request } ) => !! pull_request?.merged_at ) - .sort( ( a, b ) => new Date( a?.pull_request?.merged_at ) - new Date( b?.pull_request?.merged_at ) ); + .sort( + ( a, b ) => + new Date( a?.pull_request?.merged_at ) - + new Date( b?.pull_request?.merged_at ) + ); - console.log( 'Found the following PRs to cherry-pick (sorted by closed date in ascending order): ' ); + console.log( + 'Found the following PRs to cherry-pick (sorted by closed date in ascending order): ' + ); PRs.forEach( ( { number, title } ) => console.log( indent( `#${ number } – ${ title }` ) ) ); diff --git a/bin/generate-php-sync-issue.mjs b/bin/generate-php-sync-issue.mjs new file mode 100644 index 00000000000000..a45d77d354107c --- /dev/null +++ b/bin/generate-php-sync-issue.mjs @@ -0,0 +1,505 @@ +/** + * External dependencies + */ + +import Octokit from '@octokit/rest'; +import fs from 'fs'; + +import { fileURLToPath } from 'url'; +import nodePath, { dirname } from 'path'; + +function getArg( argName ) { + const arg = process.argv.find( ( _arg ) => + _arg.startsWith( `--${ argName }=` ) + ); + return arg ? arg.split( '=' )[ 1 ] : null; +} + +const OWNER = 'wordpress'; +const REPO = 'gutenberg'; +const MAX_MONTHS_TO_QUERY = 4; + +// The following paths will be ignored when generating the issue content. +const IGNORED_PATHS = [ + 'lib/load.php', // plugin specific code. + 'lib/experiments-page.php', // experiments are plugin specific. + 'packages/e2e-tests/plugins', // PHP files related to e2e tests only. + 'packages/block-library', // this is handled automatically. +]; + +// PRs containing the following labels will be ignored when generating the issue content. +const LABELS_TO_IGNORE = [ 'Backport from WordPress Core' ]; + +const MAX_NESTING_LEVEL = 3; + +const __filename = fileURLToPath( import.meta.url ); +const __dirname = dirname( __filename ); + +const authToken = getArg( 'token' ); +const stableWPRelease = getArg( 'wpstable' ); + +async function main() { + if ( ! authToken ) { + console.error( + 'Error. The --token argument is required. See: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token' + ); + process.exit( 1 ); + } + + if ( ! stableWPRelease ) { + console.error( + 'Error. The --wpstable argument is required. It should be the current stable WordPress release (e.g. 6.4).' + ); + process.exit( 1 ); + } + + const sinceArg = getArg( 'since' ); + let since; + + if ( sinceArg ) { + if ( validateDate( sinceArg ) ) { + since = sinceArg; + } else { + console.error( + `Error: The --since argument cannot be more than ${ MAX_MONTHS_TO_QUERY } months from the current date.` + ); + process.exit( 1 ); + } + } else { + console.error( + `Error. The --since argument is required (e.g. YYYY-MM-DD). This should be the date of the final Gutenberg release that was included in the last stable WP Core release (see https://developer.wordpress.org/block-editor/contributors/versions-in-wordpress/).` + ); + process.exit( 1 ); + } + + const lastRcDateArg = getArg( 'lastrcdate' ); + let lastRcDate; + + if ( lastRcDateArg ) { + if ( + validateDate( lastRcDateArg ) && + isAfter( lastRcDateArg, since ) + ) { + lastRcDate = lastRcDateArg; + } else { + console.error( + `Error: The --lastrcdate argument must be a date after the --since date.` + ); + process.exit( 1 ); + } + } else { + console.error( + `Error: The --lastrcdate argument is required (e.g. YYYY-MM-DD).` + ); + process.exit( 1 ); + } + + console.log( 'Welcome to the PHP Sync Issue Generator!' ); + + console.log( '--------------------------------' ); + console.log( '• Running script...' ); + + // These should be paths where we expect to find PHP files that + // will require syncing to WordPress Core. This list should be + // extremely selective. + const paths = [ '/lib', '/phpunit' ]; + + console.log( `• Fetching all commits made to ${ REPO } since: ${ since }` ); + let commits = await fetchAllCommitsFromPaths( since, paths ); + + // Remove identical commits based on sha + commits = commits.reduce( ( acc, current ) => { + const x = acc.find( ( item ) => item.sha === current.sha ); + if ( ! x ) { + return acc.concat( [ current ] ); + } + return acc; + }, [] ); + + // Fetch the full commit data for each of the commits. + // This is because the /commits endpoint does not include the + // information about the `files` modified in the commit. + console.log( + `• Fetching full commit data for ${ commits.length } commits` + ); + const commitsWithCommitData = await Promise.all( + commits.map( async ( commit ) => { + const commitData = await fetchCommit( commit.sha ); + + // In the future we will want to exclude PRs based on label + // so we will need to fetch the full PR data for each commit. + // For now we can just set this to null. + const fullPRData = null; + // const fullPRData = getPullRequestDataForCommit( commit.sha ); + + // Our Issue links to the PRs associated with the commits so we must + // provide this data. We could also get the PR data from the commit data, + // using getPullRequestDataForCommit, but that requires yet another + // network request. Therefore we optimise for trying to build + // the PR URL from the commit data we have available. + commitData.pullRequest = { + url: fullPRData?.html_url || buildPRURL( commit ), + creator: + fullPRData?.user?.login || + commit?.author?.login || + 'unknown', + labels: fullPRData?.labels || [], + }; + + // if the PR labels contain any of the labels to ignore, skip this commit + // by returning null. + if ( + commitData.pullRequest.labels.some( ( label ) => + LABELS_TO_IGNORE.includes( label.name ) + ) + ) { + return null; + } + + // This is temporarily required because PRs merged between Beta 1 (since) + // and the final RC may have already been manually backported to Core. + // This is however no reliable way to identify these PRs as the `Backport to WP beta/RC` + // label is manually removed once the PR has been backported. + // In future releases we will add a **new** label `Backported` + // to indicate the PR was backported to Core. + // As a result, in the future we will be able to exclude any PRs that have + // already been backported using the `Backported` label. + if ( isAfter( lastRcDate, commitData.commit.committer.date ) ) { + commitData.isBeforeLastRCDate = true; + } + + return commitData; + } ) + ); + + const processResult = pipe( + processCommits, + reduceNesting, + dedupePRsPerLevel, + removeEmptyLevels, + sortLevels + ); + + console.log( `• Processing ${ commitsWithCommitData.length } commits` ); + const result = processResult( commitsWithCommitData ); + + console.log( `• Generating Issue content` ); + const content = generateIssueContent( result ); + + // Write the Markdown content to a file + fs.writeFileSync( nodePath.join( __dirname, 'issueContent.md' ), content ); +} + +/** + * Checks if the first date is after the second date. + * + * @param {string} date1 - The first date. + * @param {string} date2 - The second date. + * @return {boolean} - Returns true if the first date is after the second date, false otherwise. + */ +function isAfter( date1, date2 ) { + return new Date( date1 ) > new Date( date2 ); +} + +function validateDate( sinceArg ) { + const sinceDate = new Date( sinceArg ); + const maxPreviousDate = new Date(); + maxPreviousDate.setMonth( + maxPreviousDate.getMonth() - MAX_MONTHS_TO_QUERY + ); + + return sinceDate >= maxPreviousDate; +} + +async function octokitPaginate( method, params ) { + return octokitRequest( method, params, { paginate: true } ); +} + +async function octokitRequest( method = '', params = {}, settings = {} ) { + const octokit = new Octokit( { auth: authToken } ); + params.owner = OWNER; + params.repo = REPO; + + const requestType = settings?.paginate ? 'paginate' : 'request'; + + try { + const result = await octokit[ requestType ]( method, params ); + + if ( requestType === 'paginate' ) { + return result; + } + return result.data; + } catch ( error ) { + console.error( + `Error making request to ${ method }: ${ error.message }` + ); + process.exit( 1 ); + } +} + +async function fetchAllCommitsFromPaths( since, paths ) { + let commits = []; + + for ( const path of paths ) { + const pathCommits = await fetchAllCommits( since, path ); + commits = [ ...commits, ...pathCommits ]; + } + + return commits; +} + +function buildPRURL( commit ) { + const prIdMatch = commit.commit.message.match( /\(#(\d+)\)/ ); + const prId = prIdMatch ? prIdMatch[ 1 ] : null; + return prId + ? `https://github.com/WordPress/gutenberg/pull/${ prId }` + : `[Commit](${ commit.html_url })`; +} + +function sortLevels( data ) { + function processLevel( levelData ) { + const processedData = {}; + + // Separate directories and files + const directories = {}; + const files = {}; + + for ( const [ key, value ] of Object.entries( levelData ) ) { + if ( key.endsWith( '.php' ) ) { + files[ key ] = Array.isArray( value ) + ? value + : processLevel( value ); + } else { + directories[ key ] = Array.isArray( value ) + ? value + : processLevel( value ); + } + } + + // Combine directories and files + Object.assign( processedData, directories, files ); + + return processedData; + } + + return processLevel( data ); +} + +function removeEmptyLevels( data ) { + function processLevel( levelData ) { + const processedData = {}; + + for ( const [ key, value ] of Object.entries( levelData ) ) { + if ( Array.isArray( value ) ) { + if ( value.length > 0 ) { + processedData[ key ] = value; + } + } else { + const processedLevel = processLevel( value ); + if ( Object.keys( processedLevel ).length > 0 ) { + processedData[ key ] = processedLevel; + } + } + } + + return processedData; + } + + return processLevel( data ); +} + +function dedupePRsPerLevel( data ) { + function processLevel( levelData ) { + const processedData = {}; + const prSet = new Set(); + + for ( const [ key, value ] of Object.entries( levelData ) ) { + if ( Array.isArray( value ) ) { + processedData[ key ] = value.filter( ( commit ) => { + if ( ! prSet.has( commit.pullRequest.url ) ) { + prSet.add( commit.pullRequest.url ); + return true; + } + return false; + } ); + } else { + processedData[ key ] = processLevel( value ); + } + } + + return processedData; + } + + return processLevel( data ); +} + +function reduceNesting( data ) { + function processLevel( levelData, level = 1 ) { + const processedData = {}; + + for ( const [ key, value ] of Object.entries( levelData ) ) { + if ( Array.isArray( value ) ) { + processedData[ key ] = value; + } else if ( level < MAX_NESTING_LEVEL ) { + processedData[ key ] = processLevel( value, level + 1 ); + } else { + processedData[ key ] = flattenData( value ); + } + } + + return processedData; + } + + function flattenData( nestedData ) { + let flatData = []; + + for ( const value of Object.values( nestedData ) ) { + if ( Array.isArray( value ) ) { + flatData = [ ...flatData, ...value ]; + } else { + flatData = [ ...flatData, ...flattenData( value ) ]; + } + } + + return flatData; + } + + return processLevel( data ); +} + +function processCommits( commits ) { + const result = {}; + + // This dir sholud be ignored, since whatever is in there is already in core. + // It exists to provide compatibility for older releases, because we have to + // support the current and the previous WP versions. + // See: https://github.com/WordPress/gutenberg/pull/57890#pullrequestreview-1828994247. + const prevReleaseCompatDirToIgnore = `lib/compat/wordpress-${ stableWPRelease }`; + + commits.forEach( ( commit ) => { + // Skip commits without an associated pull request + if ( ! commit?.pullRequest ) { + return; + } + commit.files.forEach( ( file ) => { + // Skip files that are not PHP files. + if ( ! file.filename.endsWith( '.php' ) ) { + return; + } + + if ( + [ ...IGNORED_PATHS, prevReleaseCompatDirToIgnore ].some( + ( path ) => + file.filename.startsWith( path ) || + file.filename === path + ) + ) { + // Skip files within specific packages. + return; + } + + const parts = file.filename.split( '/' ); + + let current = result; + + // If the file is under 'phpunit', always add it to the 'phpunit' key + // as it's helpful to have a full list of commits that modify tests. + if ( parts.includes( 'phpunit' ) ) { + current.phpunit = current.phpunit || []; + current.phpunit = [ ...current.phpunit, commit ]; + } + + for ( let i = 0; i < parts.length; i++ ) { + const part = parts[ i ]; + + if ( i === parts.length - 1 ) { + current[ part ] = current[ part ] || []; + current[ part ] = [ ...current[ part ], commit ]; + } else { + current[ part ] = current[ part ] || {}; + current = current[ part ]; + } + } + } ); + } ); + + return result; +} + +function formatPRLine( { pullRequest: pr, isBeforeLastRCDate } ) { + return `- [ ] ${ pr.url } - @${ + pr.creator + } | Trac ticket | Core backport PR ${ + isBeforeLastRCDate ? '(⚠️ Check for existing WP Core backport)' : '' + }\n`; +} + +function formatHeading( level, key ) { + const emoji = key.endsWith( '.php' ) ? '📄' : '📁'; + return `${ '#'.repeat( level ) } ${ emoji } ${ key }\n\n`; +} + +function generateIssueContent( result, level = 1 ) { + let issueContent = ''; + let isFirstSection = true; + + for ( const [ key, value ] of Object.entries( result ) ) { + // Add horizontal rule divider between sections, but not before the first section + if ( level <= 2 && ! isFirstSection ) { + issueContent += '\n---\n'; + } + + issueContent += formatHeading( level, key ); + + if ( Array.isArray( value ) ) { + value.forEach( ( commit ) => { + issueContent += formatPRLine( commit ); + } ); + } else { + issueContent += generateIssueContent( value, level + 1 ); + } + + isFirstSection = false; + } + + return issueContent; +} + +async function fetchAllCommits( since, path ) { + return await octokitPaginate( 'GET /repos/{owner}/{repo}/commits', { + since, + per_page: 30, + path, + } ); +} + +async function fetchCommit( sha ) { + return octokitRequest( 'GET /repos/{owner}/{repo}/commits/{sha}', { + sha, + } ); +} + +// eslint-disable-next-line no-unused-vars +async function getPullRequestDataForCommit( commitSha ) { + const pullRequests = octokitRequest( + 'GET /repos/{owner}/{repo}/commits/{commit_sha}/pulls', + { + commit_sha: commitSha, + } + ); + + // If a related Pull Request is found, return its URL and creator + if ( pullRequests.length > 0 ) { + const pullRequest = pullRequests[ 0 ]; + return pullRequest; + } + + return null; +} + +const pipe = + ( ...fns ) => + ( x ) => + fns.reduce( ( v, f ) => f( v ), x ); + +main(); diff --git a/bin/list-experimental-api-matches.sh b/bin/list-experimental-api-matches.sh index 156464c4e7375e..d9399e63e5cf64 100755 --- a/bin/list-experimental-api-matches.sh +++ b/bin/list-experimental-api-matches.sh @@ -31,7 +31,7 @@ namespace() { awk -F: ' { print module($1), $2 } function module(path) { - n = split(path, parts, "/") + split(path, parts, "/") if (parts[1] == "lib") return "lib" return parts[1] "/" parts[2] }' diff --git a/changelog.txt b/changelog.txt index 9268dc7edd1fba..48396a9eec777d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,8 +1,41 @@ == Changelog == -= 17.5.0-rc.1 = += 17.5.2 = +## Changelog + +### Bug Fixes + +- (Preferences)(17.5.1)(fix) Remove non-core-migrated preferences from the deprecation proxy for `get` ([58153](https://github.com/WordPress/gutenberg/pull/58153)) +- (editor)(fix) Append the `edit-post-header-toolbar` class in NavigableToolbar for backward compatibility with plugin GUI injections ([58154](https://github.com/WordPress/gutenberg/pull/58154)) + +## Contributors + +The following contributors merged PRs in this release: +@fullofcaffeine + + + + += 17.5.1 = + +## Changelog + +### Bug Fixes + +- (Preferences)(hotfix)(17.5) Hotfix for missing preferences in the `core` scope([58031](https://github.com/WordPress/gutenberg/pull/58031)) + +## Contributors + +The following contributors merged PRs in this release: + +@youknowriad @fullofcaffeine + + + + += 17.5.0 = ## Changelog @@ -207,7 +240,7 @@ - Add drag cursor to draggable list items. ([57493](https://github.com/WordPress/gutenberg/pull/57493)) ### Tools - +- DependencyExtractionWebpackPlugin: Throw when using scripts from modules. ([57795] - Dependency Extraction Webpack Plugin: Use `import` for module externals. ([57577](https://github.com/WordPress/gutenberg/pull/57577)) - DependencyExtractionWebpackPlugin: Add true shorthand for requestToExternalModule. ([57593](https://github.com/WordPress/gutenberg/pull/57593)) - DependencyExtractionWebpackPlugin: Use module for @wordpress/interactivity. ([57602](https://github.com/WordPress/gutenberg/pull/57602)) @@ -256,6 +289,8 @@ The following contributors merged PRs in this release: @afercia @andrewhayward @andrewserong @atachibana @c4rl0sbr4v0 @carolinan @chad1008 @ciampo @DAreRodz @dcalhoun @derekblank @desrosj @ellatrix @fai-sal @fluiddot @geriux @getdave @glendaviesnz @gziolo @hbhalodia @HrithikDalal @jameskoster @jeryj @jorgefilipecosta @jsnajdr @juanmaguitar @kevin940726 @Mamaduka @matiasbenedetto @mcsf @michalczaplinski @mirka @muhme @ndiego @ntsekouras @oandregal @ockham @ramonjd @scruffian @sirreal @Soean @t-hamano @talldan @tellthemachines @youknowriad + + = 17.3.2 = ## Changelog diff --git a/docs/README.md b/docs/README.md index ba6b35a761f6e0..94f640ae46bfcf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # Block Editor Handbook -👋 Welcome to the Block Editor Handbook. +Welcome to the Block Editor Handbook. The [**Block Editor**](https://wordpress.org/gutenberg/) is a modern and up-to-date paradigm for WordPress site building and publishing. It uses a modular system of **Blocks** to compose and format content and is designed to create rich and flexible layouts for websites and digital products. diff --git a/docs/contributors/code/back-merging-to-wp-core.md b/docs/contributors/code/back-merging-to-wp-core.md new file mode 100644 index 00000000000000..2b1ec77df1e550 --- /dev/null +++ b/docs/contributors/code/back-merging-to-wp-core.md @@ -0,0 +1,31 @@ +# Back-merging code to WordPress Core + +For major releases of the WordPress software, Gutenberg features need to be merged into WordPress Core. Typically this involves taking changes made in `.php` files within the Gutenberg repository and making the equivalent updates in the WP Core codebase. + +## Files/Directories + +Changes to files within the following files/directories will typically require back-merging to WP Core: + +- `lib/` +- `phpunit/` + +## Ignored directories/files + +The following directories/files do _not_ require back-merging to WP Core: + +- `lib/load.php` - Plugin specific code. +- `lib/experiments-page.php` - experiments are Plugin specific. +- `packages/block-library` - this is handled automatically during the packages sync process. +- `packages/e2e-tests/plugins` - PHP files related to e2e tests only. Mostly fixture data generators. +- `phpunit/blocks` - the code is maintained in Gutenberg so the test should be as well. + +Please note this list is not exhaustive. + +## Pull Request Criteria + +In general, all PHP code committed to the Gutenberg repository since the date of the final Gutenberg release that was included in [the _last_ stable WP Core release](https://developer.wordpress.org/block-editor/contributors/versions-in-wordpress/) should be considered for back merging to WP Core. + +There are however certain exceptions to that rule. PRs with the following criteria do _not_ require back-merging to WP Core: + +- Does not contain changes to PHP code. +- Has label `Backport from WordPress Core` - this code is already in WP Core. diff --git a/docs/contributors/versions-in-wordpress.md b/docs/contributors/versions-in-wordpress.md index 4449f13996c629..ce9eb458e73723 100644 --- a/docs/contributors/versions-in-wordpress.md +++ b/docs/contributors/versions-in-wordpress.md @@ -6,6 +6,7 @@ If anything looks incorrect here, please bring it up in #core-editor in [WordPre | Gutenberg Versions | WordPress Version | | ------------------ | ----------------- | +| 16.2-16.7 | 6.4.2 | | 16.2-16.7 | 6.4.1 | | 16.2-16.7 | 6.4 | | 15.2-16.1 | 6.3.1 | diff --git a/docs/getting-started/devenv/README.md b/docs/getting-started/devenv/README.md index 47113c84d78dac..b9acda48eab7fb 100644 --- a/docs/getting-started/devenv/README.md +++ b/docs/getting-started/devenv/README.md @@ -30,7 +30,7 @@ Node.js and its accompanying development tools allow you to: The list goes on. While modern JavaScript development can be challenging, WordPress provides several tools, like [`wp-scripts`](/docs/getting-started/devenv/get-started-with-wp-scripts.md) and [`create-block`](/docs/getting-started/devenv/get-started-with-create-block.md), that streamline the process and are made possible by Node.js development tools. -**The recommended Node.js version for block development is [Active LTS](https://nodejs.dev/en/about/releases/) (Long Term Support)**. However, there are times when you need to use different versions. A Node.js version manager tool like `nvm` is strongly recommended and allows you to easily change your `node` version when required. You will also need Node Package Manager (`npm`) and the Node Package eXecute (`npx`) to work with some WordPress packages. Both are installed automatically with Node.js. +**The recommended Node.js version for block development is [Active LTS](https://nodejs.org/en/about/previous-releases) (Long Term Support)**. However, there are times when you need to use different versions. A Node.js version manager tool like `nvm` is strongly recommended and allows you to easily change your `node` version when required. You will also need Node Package Manager (`npm`) and the Node Package eXecute (`npx`) to work with some WordPress packages. Both are installed automatically with Node.js. To be able to use the Node.js tools and [packages provided by WordPress](https://github.com/WordPress/gutenberg/tree/trunk/packages) for block development, you'll need to set a proper Node.js runtime environment on your machine. To learn more about how to do this, refer to the links below. diff --git a/docs/getting-started/devenv/nodejs-development-environment.md b/docs/getting-started/devenv/nodejs-development-environment.md index 6e1d3638f5f03c..8eb72bd88268da 100644 --- a/docs/getting-started/devenv/nodejs-development-environment.md +++ b/docs/getting-started/devenv/nodejs-development-environment.md @@ -22,7 +22,7 @@ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash ``` 2. Quit and restart the terminal. -3. Run `nvm install --lts` in the terminal to install the latest [LTS](https://nodejs.dev/en/about/releases/) (Long Term Support) version of Node.js. +3. Run `nvm install --lts` in the terminal to install the latest [LTS](https://nodejs.org/en/about/previous-releases) (Long Term Support) version of Node.js. 4. Run `node -v` and `npm -v` in the terminal to verify the installed `node` and `npm` versions. If needed, you can also install specific versions of `node`. For example, install version 18 by running `nvm install 18`, and switch between different versions by running `nvm use [version-number]`. See the `nvm` [usage guide](https://github.com/nvm-sh/nvm#usage) for more details. diff --git a/docs/getting-started/fundamentals/block-wrapper.md b/docs/getting-started/fundamentals/block-wrapper.md index 1f2404eca9b031..cf588cf33cf6f8 100644 --- a/docs/getting-started/fundamentals/block-wrapper.md +++ b/docs/getting-started/fundamentals/block-wrapper.md @@ -2,7 +2,7 @@ Each block's markup is wrapped by a container HTML tag that needs to have the proper attributes to fully work in the Block Editor and to reflect the proper block's style settings when rendered in the Block Editor and the front end. As developers, we have full control over the block's markup, and WordPress provides the tools to add the attributes that need to exist on the wrapper to our block's markup. -Ensuring proper attributes to the block wrapper is especially important when using custom styling or features like `supports`. +Ensuring proper attributes to the block wrapper is especially important when using custom styling or features like `supports`.
The use of supports generates a set of properties that need to be manually added to the wrapping element of the block so they're properly stored as part of the block data. @@ -10,8 +10,8 @@ The use of supports generates a set of properties that need to be m A block can have three sets of markup defined, each one of them with a specific target and purpose: -- The one for the **Block Editor**, defined through a `edit` React component passed to [`registerBlockType`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/#registerblocktype) when registering the block in the client. -- The one used to **save the block in the DB**, defined through a `save` function passed to `registerBlockType` when registering the block in the client. +- The one for the **Block Editor**, defined through a `edit` React component passed to [`registerBlockType`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/#registerblocktype) when registering the block in the client. +- The one used to **save the block in the DB**, defined through a `save` function passed to `registerBlockType` when registering the block in the client. - This markup will be returned to the front end on request if no dynamic render has been defined for the block. - The one used to **dynamically render the markup of the block** returned to the front end on request, defined through the `render_callback` on [`register_block_type`](https://developer.wordpress.org/reference/functions/register_block_type/) or the [`render`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#render) PHP file in `block.json` - If defined, this server-side generated markup will be returned to the front end, ignoring the markup stored in DB. @@ -21,13 +21,13 @@ For the [`edit` React component and the `save` function](https://developer.wordp ## The Edit component's markup -The [`useBlockProps()`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops) hook available on the [`@wordpress/block-editor`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor) allows passing the required attributes for the Block Editor to the `edit` block's outer wrapper. +The [`useBlockProps()`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/#useblockprops) hook available on the [`@wordpress/block-editor`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor) allows passing the required attributes for the Block Editor to the `edit` block's outer wrapper. Among other things, the `useBlockProps()` hook takes care of including in this wrapper: -- An `id` for the block's markup -- Some accesibility and `data-` attributes +- An `id` for the block's markup +- Some accessibility and `data-` attributes - Classes and inline styles reflecting custom settings, which include by default: - - The `wp-block` class + - The `wp-block` class - A class that contains the name of the block with its namespace For example, for the following piece of code of a block's registration in the client... @@ -43,18 +43,18 @@ _(see the [code above](https://github.com/WordPress/block-development-examples/b ...the markup of the block in the Block Editor could look like this: ```html -

Hello World - Block Editor

@@ -87,16 +87,16 @@ _(see the [code above](https://github.com/WordPress/block-development-examples/b

Hello World – Frontend

``` -Any additional classes and attributes for the `save` function of the block should be passed as an argument of `useBlockProps.save()` (see [example](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/stylesheets-79a4c3/src/save.js)). +Any additional classes and attributes for the `save` function of the block should be passed as an argument of `useBlockProps.save()` (see [example](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/stylesheets-79a4c3/src/save.js)). When you add `supports` for any feature, the proper classes get added to the object returned by the `useBlockProps.save()` hook. ```html

Hello World

``` @@ -105,10 +105,10 @@ _(check the [example](https://github.com/WordPress/block-development-examples/tr ## The server-side render markup -Any markup in the server-side render definition for the block can use the [`get_block_wrapper_attributes()`](https://developer.wordpress.org/reference/functions/get_block_wrapper_attributes/) function to generate the string of attributes required to reflect the block settings (see [example](https://github.com/WordPress/block-development-examples/blob/f68640f42d993f0866d1879f67c73910285ca114/plugins/block-dynamic-rendering-64756b/src/render.php#L11)). +Any markup in the server-side render definition for the block can use the [`get_block_wrapper_attributes()`](https://developer.wordpress.org/reference/functions/get_block_wrapper_attributes/) function to generate the string of attributes required to reflect the block settings (see [example](https://github.com/WordPress/block-development-examples/blob/f68640f42d993f0866d1879f67c73910285ca114/plugins/block-dynamic-rendering-64756b/src/render.php#L11)). ```php

>

-``` \ No newline at end of file +``` diff --git a/docs/getting-started/quick-start-guide.md b/docs/getting-started/quick-start-guide.md index 736a56c006c9e1..d218d0ee806070 100644 --- a/docs/getting-started/quick-start-guide.md +++ b/docs/getting-started/quick-start-guide.md @@ -1,6 +1,8 @@ # Quick Start Guide -This guide is designed to demonstrate the basic principles of block development in WordPress using a hands-on approach. Following the steps below, you will create a custom block plugin that uses modern JavaScript (ESNext and JSX) in a matter of minutes. The example block displays the copyright symbol (©) and the current year, the perfect addition to any website's footer. +This guide is designed to demonstrate the basic principles of block development in WordPress using a hands-on approach. Following the steps below, you will create a custom block plugin that uses modern JavaScript (ESNext and JSX) in a matter of minutes. The example block displays the copyright symbol (©) and the current year, the perfect addition to any website's footer. You can see these steps in action through this short video demonstration. + + ## Scaffold the block plugin diff --git a/docs/how-to-guides/block-tutorial/extending-the-query-loop-block.md b/docs/how-to-guides/block-tutorial/extending-the-query-loop-block.md index 9be4f3a993d203..d3628d991f872f 100644 --- a/docs/how-to-guides/block-tutorial/extending-the-query-loop-block.md +++ b/docs/how-to-guides/block-tutorial/extending-the-query-loop-block.md @@ -208,13 +208,13 @@ export const withBookQueryControls = ( BlockEdit ) => ( props ) => { // function to handle that. return isMyBooksVariation( props ) ? ( <> - + { /** Our custom component */ } ) : ( - + ); }; diff --git a/docs/manifest.json b/docs/manifest.json index 67b8fac99f7137..e8c2e0d9d2b0f0 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1673,6 +1673,12 @@ "markdown_source": "../packages/icons/README.md", "parent": "packages" }, + { + "title": "@wordpress/interactivity-router", + "slug": "packages-interactivity-router", + "markdown_source": "../packages/interactivity-router/README.md", + "parent": "packages" + }, { "title": "@wordpress/interactivity", "slug": "packages-interactivity", diff --git a/docs/reference-guides/block-api/block-patterns.md b/docs/reference-guides/block-api/block-patterns.md index 87e268a49f6f56..b0096e24791de8 100644 --- a/docs/reference-guides/block-api/block-patterns.md +++ b/docs/reference-guides/block-api/block-patterns.md @@ -1,18 +1,6 @@ # Patterns -Block Patterns are predefined block layouts, available from the patterns tab of the block inserter. Once inserted into content, the blocks are ready for additional or modified content and configuration. - -In this Document: -- [Patterns](#patterns) - - [Block Patterns](#block-patterns) - - [register_block_pattern](#register_block_pattern) - - [Unregistering Block Patterns](#unregistering-block-patterns) - - [unregister_block_pattern](#unregister_block_pattern) - - [Block Pattern Categories](#block-pattern-categories) - - [register_block_pattern_category](#register_block_pattern_category) - - [unregister_block_pattern_category](#unregister_block_pattern_category) - - [Block patterns contextual to block types and pattern transformations](#block-patterns-contextual-to-block-types-and-pattern-transformations) - - [Semantic block patterns](#semantic-block-patterns) +Block Patterns are predefined block layouts available from the patterns tab of the block inserter. Once inserted into content, the blocks are ready for additional or modified content and configuration. ## Block patterns @@ -33,12 +21,12 @@ The properties available for block patterns are: - `keywords` (optional): An array of aliases or keywords that help users discover the pattern while searching. - `viewportWidth` (optional): An integer specifying the intended width of the pattern to allow for a scaled preview of the pattern in the inserter. - `blockTypes` (optional): An array of block types that the pattern is intended to be used with. Each value needs to be the declared block's `name`. -- `postTypes` (optional): An array of post types that the pattern is restricted to be used with. The pattern will only be available when editing one of the post types passed on the array, for all the other post types the pattern is not available at all. -- `templateTypes` (optional): An array of template types where the pattern makes sense e.g: '404' if the pattern is for a 404 page, single-post if the pattern is for showing a single post. +- `postTypes` (optional): An array of post types that the pattern is restricted to be used with. The pattern will only be available when editing one of the post types passed on the array. For all the other post types, the pattern is not available at all. +- `templateTypes` (optional): An array of template types where the pattern makes sense, for example, `404` if the pattern is for a 404 page, `single-post` if the pattern is for showing a single post. - `inserter` (optional): By default, all patterns will appear in the inserter. To hide a pattern so that it can only be inserted programmatically, set the `inserter` to `false`. -- `source` (optional): A string that denotes the source of the pattern. For a plugin registering a pattern, pass the string 'plugin'. For a theme, pass the string 'theme'. +- `source` (optional): A string that denotes the source of the pattern. For a plugin registering a pattern, pass the string `plugin`. For a theme, pass the string `theme`. -The following code sample registers a block pattern named 'my-plugin/my-awesome-pattern': +The following code sample registers a block pattern named `my-plugin/my-awesome-pattern`: ```php register_block_pattern( @@ -51,9 +39,7 @@ register_block_pattern( ); ``` -_Note:_ - -`register_block_pattern()` should be called from a handler attached to the init hook. +Note that `register_block_pattern()` should be called from a handler attached to the `init` hook. ```php function my_plugin_register_my_patterns() { @@ -67,10 +53,11 @@ add_action( 'init', 'my_plugin_register_my_patterns' ); ### unregister_block_pattern -The `unregister_block_pattern` helper function allows for a previously registered block pattern to be unregistered from a theme or plugin and receives one argument. +The `unregister_block_pattern` helper function allows a previously registered block pattern to be unregistered from a theme or plugin and receives one argument. + - `title`: The name of the block pattern to be unregistered. -The following code sample unregisters the block pattern named 'my-plugin/my-awesome-pattern': +The following code sample unregisters the block pattern named `my-plugin/my-awesome-pattern`: ```php unregister_block_pattern( 'my-plugin/my-awesome-pattern' ); @@ -95,6 +82,7 @@ Block patterns can be grouped using categories. The block editor comes with bund ### register_block_pattern_category The `register_block_pattern_category` helper function receives two arguments. + - `title`: A machine-readable title for the block pattern category. - `properties`: An array describing properties of the pattern category. @@ -102,7 +90,7 @@ The properties of the pattern categories include: - `label` (required): A human-readable label for the pattern category. -The following code sample registers the category named 'hero': +The following code sample registers the category named `hero`: ```php register_block_pattern_category( @@ -127,12 +115,11 @@ add_action( 'init', 'my_plugin_register_my_pattern_categories' ); ### unregister_block_pattern_category -`unregister_block_pattern_category` allows unregistering a pattern category. - The `unregister_block_pattern_category` helper function allows for a previously registered block pattern category to be unregistered from a theme or plugin and receives one argument. + - `title`: The name of the block pattern category to be unregistered. -The following code sample unregisters the category named 'hero': +The following code sample unregisters the category named `hero`: ```php unregister_block_pattern_category( 'hero' ); @@ -154,7 +141,7 @@ add_action( 'init', 'my_plugin_unregister_my_pattern_categories' ); It is possible to attach a block pattern to one or more block types. This adds the block pattern as an available transform for that block type. -Currently these transformations are available only to simple blocks (blocks without inner blocks). In order for a pattern to be suggested, **every selected block must be present in the block pattern**. +Currently, these transformations are available only to simple blocks (blocks without inner blocks). In order for a pattern to be suggested, **every selected block must be present in the block pattern**. For instance: @@ -171,9 +158,9 @@ register_block_pattern( ); ``` -The above code registers a block pattern named 'my-plugin/powered-by-wordpress' and also shows the pattern in the "transform menu" of paragraph blocks. The transformation result will be keeping the paragraph's existing content and also apply the other attributes - in this case the background and text color. +The above code registers a block pattern named `my-plugin/powered-by-wordpress` and shows the pattern in the "transform menu" of paragraph blocks. The transformation result will keep the paragraph's existing content and apply the other attributes - in this case, the background and text color. -As mentioned above pattern transformations for simple blocks can also work if we have selected multiple blocks and there are matching contextual patterns to these blocks. Let's see an example of a pattern where two block types are attached. +As mentioned above, pattern transformations for simple blocks can also work if we have selected multiple blocks and there are matching contextual patterns to these blocks. Let's see an example of a pattern where two block types are attached. ```php register_block_pattern( @@ -194,7 +181,7 @@ register_block_pattern( ); ``` -In the above example if we select **one of the two** block types, either a paragraph or a heading block, this pattern will be suggested by transforming the selected block using its content and will also add the remaining blocks from the pattern. If on the other hand we multi select one paragraph and one heading block, both blocks will be transformed. +In the above example, if we select **one of the two** block types, either a paragraph or a heading block, this pattern will be suggested by transforming the selected block using its content and will also add the remaining blocks from the pattern. If, on the other hand, we multi-select one paragraph and one heading block, both blocks will be transformed. Blocks can also use these contextual block patterns in other places. For instance, when inserting a new Query Loop block, the user is provided with a list of all patterns attached to the block. diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index c05cdd3eb009b1..ab6bb52b6b3b2a 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -42,7 +42,7 @@ Create and save content to reuse across your site. Update the pattern, and the c - **Name:** core/block - **Category:** reusable - **Supports:** ~~customClassName~~, ~~html~~, ~~inserter~~, ~~renaming~~ -- **Attributes:** ref +- **Attributes:** overrides, ref ## Button @@ -639,7 +639,7 @@ Displays the next or previous post link that is adjacent to the current post. ([ - **Name:** core/post-navigation-link - **Category:** theme - **Supports:** color (background, link, text), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ -- **Attributes:** arrow, label, linkLabel, showTitle, textAlign, type +- **Attributes:** arrow, inSameTerm, label, linkLabel, showTitle, taxonomy, textAlign, type ## Post Template @@ -694,7 +694,7 @@ Give special visual emphasis to a quote from your text. ([Source](https://github - **Name:** core/pullquote - **Category:** text -- **Supports:** align (full, left, right, wide), anchor, color (background, gradients, link, text), typography (fontSize, lineHeight) +- **Supports:** align (full, left, right, wide), anchor, color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight) - **Attributes:** citation, textAlign, value ## Query Loop diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 7b0bd386daaf48..b03e905d166bc0 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -409,6 +409,19 @@ _Returns_ - `WPBlock[]`: Block objects. +### getBlocksByName + +Returns all blocks that match a blockName. Results include nested blocks. + +_Parameters_ + +- _state_ `Object`: Global application state. +- _blockName_ `?string`: Optional block name, if not specified, returns an empty array. + +_Returns_ + +- `Array`: Array of clientIds of blocks with name equal to blockName. + ### getBlockSelectionEnd Returns the current block selection end. This value may be null, and it may represent either a singular block selection or multi-selection end. A selection is singular if its start and end match. diff --git a/docs/reference-guides/data/data-core-notices.md b/docs/reference-guides/data/data-core-notices.md index e11e6f226169f9..d36098429811dd 100644 --- a/docs/reference-guides/data/data-core-notices.md +++ b/docs/reference-guides/data/data-core-notices.md @@ -277,7 +277,7 @@ export const ExampleComponent = () => { const notices = useSelect( ( select ) => select( noticesStore ).getNotices() ); - const { removeNotices } = useDispatch( noticesStore ); + const { removeAllNotices } = useDispatch( noticesStore ); return ( <>
    diff --git a/docs/reference-guides/filters/block-filters.md b/docs/reference-guides/filters/block-filters.md index 4c7e3df7cec128..35a041052889c0 100644 --- a/docs/reference-guides/filters/block-filters.md +++ b/docs/reference-guides/filters/block-filters.md @@ -19,11 +19,11 @@ _Example_: ```php { return ( props ) => { return ( <> - + My custom control @@ -352,14 +352,14 @@ On the server, you can filter the list of blocks shown in the inserter using the post ) ) { return array( 'core/paragraph', 'core/heading' ); } return $allowed_block_types; } -add_filter( 'allowed_block_types_all', 'filter_allowed_block_types_when_post_provided', 10, 2 ); +add_filter( 'allowed_block_types_all', 'wpdocs_filter_allowed_block_types_when_post_provided', 10, 2 ); ``` ## Managing block categories @@ -374,7 +374,7 @@ It is possible to filter the list of default block categories using the `block_c post ) ) { array_push( $block_categories, @@ -388,7 +388,7 @@ function filter_block_categories_when_post_provided( $block_categories, $editor_ return $block_categories; } -add_filter( 'block_categories_all', 'filter_block_categories_when_post_provided', 10, 2 ); +add_filter( 'block_categories_all', 'wpdocs_filter_block_categories_when_post_provided', 10, 2 ); ``` ### `wp.blocks.updateCategory` diff --git a/docs/reference-guides/filters/editor-filters.md b/docs/reference-guides/filters/editor-filters.md index 59f6d7ef8213f3..943e161a1df49d 100644 --- a/docs/reference-guides/filters/editor-filters.md +++ b/docs/reference-guides/filters/editor-filters.md @@ -84,14 +84,14 @@ _Example:_ post ) ) { $editor_settings['maxUploadFileSize'] = 12345; } return $editor_settings; } -add_filter( 'block_editor_settings_all', 'filter_block_editor_settings_when_post_provided', 10, 2 ); +add_filter( 'block_editor_settings_all', 'wpdocs_filter_block_editor_settings_when_post_provided', 10, 2 ); ``` #### `block_editor_rest_api_preload_paths` @@ -104,14 +104,14 @@ _Example:_ post ) ) { array_push( $preload_paths, array( '/wp/v2/blocks', 'OPTIONS' ) ); } return $preload_paths; } -add_filter( 'block_editor_rest_api_preload_paths', 'filter_block_editor_rest_api_preload_paths_when_post_provided', 10, 2 ); +add_filter( 'block_editor_rest_api_preload_paths', 'wpdocs_filter_block_editor_rest_api_preload_paths_when_post_provided', 10, 2 ); ``` ### Available default editor settings diff --git a/docs/reference-guides/filters/global-styles-filters.md b/docs/reference-guides/filters/global-styles-filters.md index 7d3b6be95768b9..59bbbfcd5921dd 100644 --- a/docs/reference-guides/filters/global-styles-filters.md +++ b/docs/reference-guides/filters/global-styles-filters.md @@ -14,7 +14,7 @@ _Example:_ This is how to pass a new color palette for the theme and disable the text color UI: ```php -function filter_theme_json_theme( $theme_json ){ +function wpdocs_filter_theme_json_theme( $theme_json ){ $new_data = array( 'version' => 2, 'settings' => array( @@ -38,5 +38,5 @@ function filter_theme_json_theme( $theme_json ){ return $theme_json->update_with( $new_data ); } -add_filter( 'wp_theme_json_data_theme', 'filter_theme_json_theme' ); +add_filter( 'wp_theme_json_data_theme', 'wpdocs_filter_theme_json_theme' ); ``` diff --git a/docs/reference-guides/filters/parser-filters.md b/docs/reference-guides/filters/parser-filters.md index 3acfb2489182db..7adc68cc0bf7e7 100644 --- a/docs/reference-guides/filters/parser-filters.md +++ b/docs/reference-guides/filters/parser-filters.md @@ -26,11 +26,11 @@ class EmptyParser { } } -function my_plugin_select_empty_parser( $prev_parser_class ) { +function wpdocs_select_empty_parser( $prev_parser_class ) { return 'EmptyParser'; } -add_filter( 'block_parser_class', 'my_plugin_select_empty_parser', 10, 1 ); +add_filter( 'block_parser_class', 'wpdocs_select_empty_parser', 10, 1 ); ``` > **Note**: At the present time it's not possible to replace the client-side parser. diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index 4baa5a6009ded6..ee88f779ace1ce 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -176,6 +176,7 @@ Settings related to typography. | Property | Type | Default | Props | | --- | --- | --- |--- | +| defaultFontSizes | boolean | true | | | customFontSize | boolean | true | | | fontStyle | boolean | true | | | fontWeight | boolean | true | | diff --git a/gutenberg.php b/gutenberg.php index 9559f838608da9..d78f218fe7fea8 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. * Requires at least: 6.3 * Requires PHP: 7.0 - * Version: 17.5.0-rc.1 + * Version: 17.6.0-rc.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/block-supports/pattern.php b/lib/block-supports/pattern.php deleted file mode 100644 index a783135c793e3f..00000000000000 --- a/lib/block-supports/pattern.php +++ /dev/null @@ -1,36 +0,0 @@ -supports, array( '__experimentalConnections' ), false ) : false; - - if ( $pattern_support ) { - if ( ! $block_type->uses_context ) { - $block_type->uses_context = array(); - } - - if ( ! in_array( 'pattern/overrides', $block_type->uses_context, true ) ) { - $block_type->uses_context[] = 'pattern/overrides'; - } - } - } - - // Register the block support. - WP_Block_Supports::get_instance()->register( - 'pattern', - array( - 'register_attribute' => 'gutenberg_register_pattern_support', - ) - ); -} diff --git a/lib/block-supports/shadow.php b/lib/block-supports/shadow.php index 4a28c98b79325d..87258930faf10e 100644 --- a/lib/block-supports/shadow.php +++ b/lib/block-supports/shadow.php @@ -53,9 +53,8 @@ function gutenberg_apply_shadow_support( $block_type, $block_attributes ) { $shadow_block_styles = array(); - $preset_shadow = array_key_exists( 'shadow', $block_attributes ) ? "var:preset|shadow|{$block_attributes['shadow']}" : null; - $custom_shadow = isset( $block_attributes['style']['shadow'] ) ? $block_attributes['style']['shadow'] : null; - $shadow_block_styles['shadow'] = $preset_shadow ? $preset_shadow : $custom_shadow; + $custom_shadow = $block_attributes['style']['shadow'] ?? null; + $shadow_block_styles['shadow'] = $custom_shadow; $attributes = array(); $styles = gutenberg_style_engine_get_styles( $shadow_block_styles ); diff --git a/lib/block-supports/typography.php b/lib/block-supports/typography.php index 3cda86cf0a2573..b3fa4b4252eed2 100644 --- a/lib/block-supports/typography.php +++ b/lib/block-supports/typography.php @@ -401,10 +401,18 @@ function gutenberg_get_computed_fluid_typography_value( $args = array() ) { return null; } - // Build CSS rule. - // Borrowed from https://websemantics.uk/tools/responsive-font-calculator/. + // Calculates the linear factor denominator. If it's 0, we cannot calculate a fluid value. + $linear_factor_denominator = $maximum_viewport_width['value'] - $minimum_viewport_width['value']; + if ( empty( $linear_factor_denominator ) ) { + return null; + } + + /* + * Build CSS rule. + * Borrowed from https://websemantics.uk/tools/responsive-font-calculator/. + */ $view_port_width_offset = round( $minimum_viewport_width['value'] / 100, 3 ) . $font_size_unit; - $linear_factor = 100 * ( ( $maximum_font_size['value'] - $minimum_font_size['value'] ) / ( $maximum_viewport_width['value'] - $minimum_viewport_width['value'] ) ); + $linear_factor = 100 * ( ( $maximum_font_size['value'] - $minimum_font_size['value'] ) / ( $linear_factor_denominator ) ); $linear_factor_scaled = round( $linear_factor * $scale_factor, 3 ); $linear_factor_scaled = empty( $linear_factor_scaled ) ? 1 : $linear_factor_scaled; $fluid_target_font_size = implode( '', $minimum_font_size_rem ) . " + ((1vw - $view_port_width_offset) * $linear_factor_scaled)"; diff --git a/lib/blocks.php b/lib/blocks.php index d5283afeb7f999..e1d4622a0f23da 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -440,27 +440,6 @@ function gutenberg_legacy_wp_block_post_meta( $value, $object_id, $meta_key, $si add_filter( 'default_post_metadata', 'gutenberg_legacy_wp_block_post_meta', 10, 4 ); -/** - * Registers the metadata block attribute for all block types. - * - * @param array $args Array of arguments for registering a block type. - * @return array $args - */ -function gutenberg_register_metadata_attribute( $args ) { - // Setup attributes if needed. - if ( ! isset( $args['attributes'] ) || ! is_array( $args['attributes'] ) ) { - $args['attributes'] = array(); - } - - if ( ! array_key_exists( 'metadata', $args['attributes'] ) ) { - $args['attributes']['metadata'] = array( - 'type' => 'object', - ); - } - - return $args; -} -add_filter( 'register_block_type_args', 'gutenberg_register_metadata_attribute' ); /** * Strips all HTML from the content of footnotes, and sanitizes the ID. diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index aa8de83df9597b..f4266a7ef66dd5 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -155,7 +155,7 @@ class WP_Theme_JSON_Gutenberg { ), array( 'path' => array( 'typography', 'fontSizes' ), - 'prevent_override' => false, + 'prevent_override' => array( 'typography', 'defaultFontSizes' ), 'use_default_names' => true, 'value_func' => 'gutenberg_get_typography_font_size_value', 'css_vars' => '--wp--preset--font-size--$slug', @@ -411,19 +411,20 @@ class WP_Theme_JSON_Gutenberg { 'defaultPresets' => null, ), 'typography' => array( - 'fluid' => null, - 'customFontSize' => null, - 'dropCap' => null, - 'fontFamilies' => null, - 'fontSizes' => null, - 'fontStyle' => null, - 'fontWeight' => null, - 'letterSpacing' => null, - 'lineHeight' => null, - 'textColumns' => null, - 'textDecoration' => null, - 'textTransform' => null, - 'writingMode' => null, + 'fluid' => null, + 'customFontSize' => null, + 'defaultFontSizes' => null, + 'dropCap' => null, + 'fontFamilies' => null, + 'fontSizes' => null, + 'fontStyle' => null, + 'fontWeight' => null, + 'letterSpacing' => null, + 'lineHeight' => null, + 'textColumns' => null, + 'textDecoration' => null, + 'textTransform' => null, + 'writingMode' => null, ), ); @@ -1009,7 +1010,7 @@ protected static function get_blocks_metadata() { if ( $duotone_support ) { $root_selector = wp_get_block_css_selector( $block_type ); - $duotone_selector = WP_Theme_JSON_Gutenberg::scope_selector( $root_selector, $duotone_support ); + $duotone_selector = static::scope_selector( $root_selector, $duotone_support ); } } @@ -1184,7 +1185,7 @@ public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' $setting_nodes[ $root_settings_key ]['selector'] = $options['root_selector']; } if ( false !== $root_style_key ) { - $setting_nodes[ $root_style_key ]['selector'] = $options['root_selector']; + $style_nodes[ $root_style_key ]['selector'] = $options['root_selector']; } } diff --git a/lib/client-assets.php b/lib/client-assets.php index c5b03b6833d030..717267a5e2a504 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -266,7 +266,7 @@ function gutenberg_register_packages_styles( $styles ) { $styles, 'wp-block-editor', gutenberg_url( 'build/block-editor/style.css' ), - array( 'wp-components' ), + array( 'wp-components', 'wp-preferences' ), $version ); $styles->add_data( 'wp-block-editor', 'rtl', 'replace' ); @@ -275,7 +275,7 @@ function gutenberg_register_packages_styles( $styles ) { $styles, 'wp-editor', gutenberg_url( 'build/editor/style.css' ), - array( 'wp-components', 'wp-block-editor', 'wp-patterns', 'wp-reusable-blocks' ), + array( 'wp-components', 'wp-block-editor', 'wp-patterns', 'wp-reusable-blocks', 'wp-preferences' ), $version ); $styles->add_data( 'wp-editor', 'rtl', 'replace' ); @@ -284,7 +284,7 @@ function gutenberg_register_packages_styles( $styles ) { $styles, 'wp-edit-post', gutenberg_url( 'build/edit-post/style.css' ), - array( 'wp-components', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-block-library', 'wp-commands' ), + array( 'wp-components', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-block-library', 'wp-commands', 'wp-preferences' ), $version ); $styles->add_data( 'wp-edit-post', 'rtl', 'replace' ); @@ -409,7 +409,7 @@ function gutenberg_register_packages_styles( $styles ) { $styles, 'wp-edit-site', gutenberg_url( 'build/edit-site/style.css' ), - array( 'wp-components', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-commands' ), + array( 'wp-components', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-commands', 'wp-preferences' ), $version ); $styles->add_data( 'wp-edit-site', 'rtl', 'replace' ); @@ -418,7 +418,7 @@ function gutenberg_register_packages_styles( $styles ) { $styles, 'wp-edit-widgets', gutenberg_url( 'build/edit-widgets/style.css' ), - array( 'wp-components', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-patterns', 'wp-reusable-blocks', 'wp-widgets' ), + array( 'wp-components', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-patterns', 'wp-reusable-blocks', 'wp-widgets', 'wp-preferences' ), $version ); $styles->add_data( 'wp-edit-widgets', 'rtl', 'replace' ); @@ -436,7 +436,7 @@ function gutenberg_register_packages_styles( $styles ) { $styles, 'wp-customize-widgets', gutenberg_url( 'build/customize-widgets/style.css' ), - array( 'wp-components', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-widgets' ), + array( 'wp-components', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-widgets', 'wp-preferences' ), $version ); $styles->add_data( 'wp-customize-widgets', 'rtl', 'replace' ); @@ -466,6 +466,15 @@ function gutenberg_register_packages_styles( $styles ) { array( 'wp-components' ) ); $styles->add_data( 'wp-widgets', 'rtl', 'replace' ); + + gutenberg_override_style( + $styles, + 'wp-preferences', + gutenberg_url( 'build/preferences/style.css' ), + array( 'wp-components' ), + $version + ); + $styles->add_data( 'wp-preferences', 'rtl', 'replace' ); } add_action( 'wp_default_styles', 'gutenberg_register_packages_styles' ); diff --git a/lib/compat/wordpress-6.5/block-bindings/block-bindings.php b/lib/compat/wordpress-6.5/block-bindings/block-bindings.php new file mode 100644 index 00000000000000..f9b33946613934 --- /dev/null +++ b/lib/compat/wordpress-6.5/block-bindings/block-bindings.php @@ -0,0 +1,163 @@ +register_source( $source_name, $source_properties ); + } +} + +/** + * Retrieves the list of registered block sources. + * + * @return array The list of registered block sources. + */ +if ( ! function_exists( 'wp_block_bindings_get_sources' ) ) { + function wp_block_bindings_get_sources() { + return wp_block_bindings()->get_sources(); + } +} + +/** + * Replaces the HTML content of a block based on the provided source value. + * + * @param string $block_content Block Content. + * @param string $block_name The name of the block to process. + * @param string $block_attr The attribute of the block we want to process. + * @param string $source_value The value used to replace the HTML. + * @return string The modified block content. + */ +function gutenberg_block_bindings_replace_html( $block_content, $block_name, $block_attr, $source_value ) { + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name ); + if ( null === $block_type ) { + return; + } + + // Depending on the attribute source, the processing will be different. + switch ( $block_type->attributes[ $block_attr ]['source'] ) { + case 'html': + case 'rich-text': + $block_reader = new WP_HTML_Tag_Processor( $block_content ); + + // TODO: Support for CSS selectors whenever they are ready in the HTML API. + // In the meantime, support comma-separated selectors by exploding them into an array. + $selectors = explode( ',', $block_type->attributes[ $block_attr ]['selector'] ); + // Add a bookmark to the first tag to be able to iterate over the selectors. + $block_reader->next_tag(); + $block_reader->set_bookmark( 'iterate-selectors' ); + + // TODO: This shouldn't be needed when the `set_inner_html` function is ready. + // Store the parent tag and its attributes to be able to restore them later in the button. + // The button block has a wrapper while the paragraph and heading blocks don't. + if ( 'core/button' === $block_name ) { + $button_wrapper = $block_reader->get_tag(); + $button_wrapper_attribute_names = $block_reader->get_attribute_names_with_prefix( '' ); + $button_wrapper_attrs = array(); + foreach ( $button_wrapper_attribute_names as $name ) { + $button_wrapper_attrs[ $name ] = $block_reader->get_attribute( $name ); + } + } + + foreach ( $selectors as $selector ) { + // If the parent tag, or any of its children, matches the selector, replace the HTML. + if ( strcasecmp( $block_reader->get_tag( $selector ), $selector ) === 0 || $block_reader->next_tag( + array( + 'tag_name' => $selector, + ) + ) ) { + $block_reader->release_bookmark( 'iterate-selectors' ); + + // TODO: Use `set_inner_html` method whenever it's ready in the HTML API. + // Until then, it is hardcoded for the paragraph, heading, and button blocks. + // Store the tag and its attributes to be able to restore them later. + $selector_attribute_names = $block_reader->get_attribute_names_with_prefix( '' ); + $selector_attrs = array(); + foreach ( $selector_attribute_names as $name ) { + $selector_attrs[ $name ] = $block_reader->get_attribute( $name ); + } + $selector_markup = "<$selector>" . wp_kses_post( $source_value ) . ""; + $amended_content = new WP_HTML_Tag_Processor( $selector_markup ); + $amended_content->next_tag(); + foreach ( $selector_attrs as $attribute_key => $attribute_value ) { + $amended_content->set_attribute( $attribute_key, $attribute_value ); + } + if ( 'core/paragraph' === $block_name || 'core/heading' === $block_name ) { + return $amended_content->get_updated_html(); + } + if ( 'core/button' === $block_name ) { + $button_markup = "<$button_wrapper>{$amended_content->get_updated_html()}"; + $amended_button = new WP_HTML_Tag_Processor( $button_markup ); + $amended_button->next_tag(); + foreach ( $button_wrapper_attrs as $attribute_key => $attribute_value ) { + $amended_button->set_attribute( $attribute_key, $attribute_value ); + } + return $amended_button->get_updated_html(); + } + } else { + $block_reader->seek( 'iterate-selectors' ); + } + } + $block_reader->release_bookmark( 'iterate-selectors' ); + return $block_content; + + case 'attribute': + $amended_content = new WP_HTML_Tag_Processor( $block_content ); + if ( ! $amended_content->next_tag( + array( + // TODO: build the query from CSS selector. + 'tag_name' => $block_type->attributes[ $block_attr ]['selector'], + ) + ) ) { + return $block_content; + } + $amended_content->set_attribute( $block_type->attributes[ $block_attr ]['attribute'], esc_attr( $source_value ) ); + return $amended_content->get_updated_html(); + break; + + default: + return $block_content; + break; + } + return; +} diff --git a/lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings.php b/lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings.php new file mode 100644 index 00000000000000..68b51348010e3a --- /dev/null +++ b/lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings.php @@ -0,0 +1,63 @@ +sources[ $source_name ] = $source_properties; + } + + /** + * Retrieves the list of registered block sources. + * + * @return array The array of registered sources. + */ + public function get_sources() { + return $this->sources; + } +} diff --git a/lib/compat/wordpress-6.5/block-bindings/sources/pattern.php b/lib/compat/wordpress-6.5/block-bindings/sources/pattern.php new file mode 100644 index 00000000000000..65ddb7278e7035 --- /dev/null +++ b/lib/compat/wordpress-6.5/block-bindings/sources/pattern.php @@ -0,0 +1,37 @@ +attributes, array( 'metadata', 'id' ), false ) ) { + return null; + } + $block_id = $block_instance->attributes['metadata']['id']; + $attribute_override = _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id, $attribute_name ), null ); + if ( null === $attribute_override ) { + return null; + } + switch ( $attribute_override[0] ) { + case 0: // remove + /** + * TODO: This currently doesn't remove the attribute, but only set it to an empty string. + * It's a temporary solution until the block binding API supports different operations. + */ + return ''; + case 1: // replace + return $attribute_override[1]; + default: + return null; + } + }; + wp_block_bindings_register_source( + 'pattern_attributes', + array( + 'label' => __( 'Pattern Attributes' ), + 'apply' => $pattern_source_callback, + ) + ); +} diff --git a/lib/compat/wordpress-6.5/block-bindings/sources/post-meta.php b/lib/compat/wordpress-6.5/block-bindings/sources/post-meta.php new file mode 100644 index 00000000000000..e52b4f289ccdd3 --- /dev/null +++ b/lib/compat/wordpress-6.5/block-bindings/sources/post-meta.php @@ -0,0 +1,26 @@ +context['postId'] but it wasn't available in the image block. + $post_id = get_the_ID(); + } + + return get_post_meta( $post_id, $source_attrs['value'], true ); + }; + wp_block_bindings_register_source( + 'post_meta', + array( + 'label' => __( 'Post Meta' ), + 'apply' => $post_meta_source_callback, + ) + ); +} diff --git a/lib/compat/wordpress-6.5/block-patterns.php b/lib/compat/wordpress-6.5/block-patterns.php index f43acda2a1035c..cce97cb19c6902 100644 --- a/lib/compat/wordpress-6.5/block-patterns.php +++ b/lib/compat/wordpress-6.5/block-patterns.php @@ -49,7 +49,7 @@ function gutenberg_register_taxonomy_patterns() { 'singular_name' => _x( 'Pattern Category', 'taxonomy singular name' ), 'add_new_item' => __( 'Add New Category' ), 'add_or_remove_items' => __( 'Add or remove pattern categories' ), - 'back_to_items' => __( '← Go to pattern categories' ), + 'back_to_items' => __( '← Go to Pattern Categories' ), 'choose_from_most_used' => __( 'Choose from the most used pattern categories' ), 'edit_item' => __( 'Edit Pattern Category' ), 'item_link' => __( 'Pattern Category Link' ), diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php new file mode 100644 index 00000000000000..e1b91364fe22e5 --- /dev/null +++ b/lib/compat/wordpress-6.5/blocks.php @@ -0,0 +1,126 @@ + 'object', + ); + } + + return $args; +} +add_filter( 'register_block_type_args', 'gutenberg_register_metadata_attribute' ); + + +if ( ! function_exists( 'gutenberg_process_block_bindings' ) ) { + /** + * Process the block bindings attribute. + * + * @param string $block_content Block Content. + * @param array $block Block attributes. + * @param WP_Block $block_instance The block instance. + */ + function gutenberg_process_block_bindings( $block_content, $block, $block_instance ) { + + // Allowed blocks that support block bindings. + // TODO: Look for a mechanism to opt-in for this. Maybe adding a property to block attributes? + $allowed_blocks = array( + 'core/paragraph' => array( 'content' ), + 'core/heading' => array( 'content' ), + 'core/image' => array( 'url', 'title', 'alt' ), + 'core/button' => array( 'url', 'text', 'linkTarget' ), + ); + + // If the block doesn't have the bindings property or isn't one of the allowed block types, return. + if ( ! isset( $block['attrs']['metadata']['bindings'] ) || ! isset( $allowed_blocks[ $block_instance->name ] ) ) { + return $block_content; + } + + /* + * Assuming the following format for the bindings property of the "metadata" attribute: + * + * "bindings": { + * "title": { + * "source": { + * "name": "post_meta", + * "attributes": { "value": "text_custom_field" } + * } + * }, + * "url": { + * "source": { + * "name": "post_meta", + * "attributes": { "value": "text_custom_field" } + * } + * } + * } + */ + + $block_bindings_sources = wp_block_bindings_get_sources(); + $modified_block_content = $block_content; + foreach ( $block['attrs']['metadata']['bindings'] as $binding_attribute => $binding_source ) { + + // If the attribute is not in the list, process next attribute. + if ( ! in_array( $binding_attribute, $allowed_blocks[ $block_instance->name ], true ) ) { + continue; + } + // If no source is provided, or that source is not registered, process next attribute. + if ( ! isset( $binding_source['source'] ) || ! isset( $binding_source['source']['name'] ) || ! isset( $block_bindings_sources[ $binding_source['source']['name'] ] ) ) { + continue; + } + + $source_callback = $block_bindings_sources[ $binding_source['source']['name'] ]['apply']; + // Get the value based on the source. + if ( ! isset( $binding_source['source']['attributes'] ) ) { + $source_args = array(); + } else { + $source_args = $binding_source['source']['attributes']; + } + $source_value = $source_callback( $source_args, $block_instance, $binding_attribute ); + // If the value is null, process next attribute. + if ( is_null( $source_value ) ) { + continue; + } + + // Process the HTML based on the block and the attribute. + $modified_block_content = gutenberg_block_bindings_replace_html( $modified_block_content, $block_instance->name, $binding_attribute, $source_value ); + } + return $modified_block_content; + } +} + +add_filter( 'render_block', 'gutenberg_process_block_bindings', 20, 3 ); diff --git a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php deleted file mode 100644 index 9c270f59fa220e..00000000000000 --- a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php +++ /dev/null @@ -1,664 +0,0 @@ -. - * - * @var array - */ - private static $nav_blocks_wrapped_in_list_item = array( - 'core/navigation-link', - 'core/home-link', - 'core/site-title', - 'core/site-logo', - 'core/navigation-submenu', - ); - - /** - * Used to determine which blocks need an
  • wrapper. - * - * @var array - */ - private static $needs_list_item_wrapper = array( - 'core/site-title', - 'core/site-logo', - ); - - /** - * Keeps track of all the navigation names that have been seen. - * - * @var array - */ - private static $seen_menu_names = array(); - - /** - * Returns whether or not this is responsive navigation. - * - * @param array $attributes The block attributes. - * @return bool Returns whether or not this is responsive navigation. - */ - private static function is_responsive( $attributes ) { - /** - * This is for backwards compatibility after the `isResponsive` attribute was been removed. - */ - - $has_old_responsive_attribute = ! empty( $attributes['isResponsive'] ) && $attributes['isResponsive']; - return isset( $attributes['overlayMenu'] ) && 'never' !== $attributes['overlayMenu'] || $has_old_responsive_attribute; - } - - /** - * Returns whether or not a navigation has a submenu. - * - * @param WP_Block_List $inner_blocks The list of inner blocks. - * @return bool Returns whether or not a navigation has a submenu. - */ - private static function has_submenus( $inner_blocks ) { - foreach ( $inner_blocks as $inner_block ) { - $inner_block_content = $inner_block->render(); - $p = new WP_HTML_Tag_Processor( $inner_block_content ); - if ( $p->next_tag( - array( - 'name' => 'LI', - 'class_name' => 'has-child', - ) - ) ) { - return true; - } - } - return false; - } - - /** - * Determine whether to load the view script. - * - * @param array $attributes The block attributes. - * @param WP_Block_List $inner_blocks The list of inner blocks. - * @return bool Returns whether or not to load the view script. - */ - private static function should_load_view_script( $attributes, $inner_blocks ) { - $has_submenus = static::has_submenus( $inner_blocks ); - $is_responsive_menu = static::is_responsive( $attributes ); - return ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ) || $is_responsive_menu; - } - - /** - * Returns whether or not a block needs a list item wrapper. - * - * @param WP_Block $block The block. - * @return bool Returns whether or not a block needs a list item wrapper. - */ - private static function does_block_need_a_list_item_wrapper( $block ) { - return in_array( $block->name, static::$needs_list_item_wrapper, true ); - } - - /** - * Returns the markup for a single inner block. - * - * @param WP_Block $inner_block The inner block. - * @return string Returns the markup for a single inner block. - */ - private static function get_markup_for_inner_block( $inner_block ) { - $inner_block_content = $inner_block->render(); - if ( ! empty( $inner_block_content ) ) { - if ( static::does_block_need_a_list_item_wrapper( $inner_block ) ) { - return '
  • ' . $inner_block_content . '
  • '; - } - - return $inner_block_content; - } - } - - /** - * Returns the html for the inner blocks of the navigation block. - * - * @param array $attributes The block attributes. - * @param WP_Block_List $inner_blocks The list of inner blocks. - * @return string Returns the html for the inner blocks of the navigation block. - */ - private static function get_inner_blocks_html( $attributes, $inner_blocks ) { - $has_submenus = static::has_submenus( $inner_blocks ); - $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); - - $style = static::get_styles( $attributes ); - $class = static::get_classes( $attributes ); - $container_attributes = get_block_wrapper_attributes( - array( - 'class' => 'wp-block-navigation__container ' . $class, - 'style' => $style, - ) - ); - - $inner_blocks_html = ''; - $is_list_open = false; - - foreach ( $inner_blocks as $inner_block ) { - $is_list_item = in_array( $inner_block->name, static::$nav_blocks_wrapped_in_list_item, true ); - - if ( $is_list_item && ! $is_list_open ) { - $is_list_open = true; - $inner_blocks_html .= sprintf( - '
      ', - $container_attributes - ); - } - - if ( ! $is_list_item && $is_list_open ) { - $is_list_open = false; - $inner_blocks_html .= '
    '; - } - - $inner_blocks_html .= static::get_markup_for_inner_block( $inner_block ); - } - - if ( $is_list_open ) { - $inner_blocks_html .= '
'; - } - - // Add directives to the submenu if needed. - if ( $has_submenus && $should_load_view_script ) { - $tags = new WP_HTML_Tag_Processor( $inner_blocks_html ); - $inner_blocks_html = gutenberg_block_core_navigation_add_directives_to_submenu( $tags, $attributes ); - } - - return $inner_blocks_html; - } - - /** - * Gets the inner blocks for the navigation block from the navigation post. - * - * @param array $attributes The block attributes. - * @return WP_Block_List Returns the inner blocks for the navigation block. - */ - private static function get_inner_blocks_from_navigation_post( $attributes ) { - $navigation_post = get_post( $attributes['ref'] ); - if ( ! isset( $navigation_post ) ) { - return new WP_Block_List( array(), $attributes ); - } - - // Only published posts are valid. If this is changed then a corresponding change - // must also be implemented in `use-navigation-menu.js`. - if ( 'publish' === $navigation_post->post_status ) { - $parsed_blocks = parse_blocks( $navigation_post->post_content ); - - // 'parse_blocks' includes a null block with '\n\n' as the content when - // it encounters whitespace. This code strips it. - $compacted_blocks = gutenberg_block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); - - // TODO - this uses the full navigation block attributes for the - // context which could be refined. - return new WP_Block_List( $compacted_blocks, $attributes ); - } - } - - /** - * Gets the inner blocks for the navigation block from the fallback. - * - * @param array $attributes The block attributes. - * @return WP_Block_List Returns the inner blocks for the navigation block. - */ - private static function get_inner_blocks_from_fallback( $attributes ) { - $fallback_blocks = gutenberg_block_core_navigation_get_fallback_blocks(); - - // Fallback my have been filtered so do basic test for validity. - if ( empty( $fallback_blocks ) || ! is_array( $fallback_blocks ) ) { - return new WP_Block_List( array(), $attributes ); - } - - return new WP_Block_List( $fallback_blocks, $attributes ); - } - - /** - * Gets the inner blocks for the navigation block. - * - * @param array $attributes The block attributes. - * @param WP_Block $block The parsed block. - * @return WP_Block_List Returns the inner blocks for the navigation block. - */ - private static function get_inner_blocks( $attributes, $block ) { - $inner_blocks = $block->inner_blocks; - - // Ensure that blocks saved with the legacy ref attribute name (navigationMenuId) continue to render. - if ( array_key_exists( 'navigationMenuId', $attributes ) ) { - $attributes['ref'] = $attributes['navigationMenuId']; - } - - // If: - // - the gutenberg plugin is active - // - `__unstableLocation` is defined - // - we have menu items at the defined location - // - we don't have a relationship to a `wp_navigation` Post (via `ref`). - // ...then create inner blocks from the classic menu assigned to that location. - if ( - defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN && - array_key_exists( '__unstableLocation', $attributes ) && - ! array_key_exists( 'ref', $attributes ) && - ! empty( gutenberg_block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ) ) - ) { - $inner_blocks = gutenberg_block_core_navigation_get_inner_blocks_from_unstable_location( $attributes ); - } - - // Load inner blocks from the navigation post. - if ( array_key_exists( 'ref', $attributes ) ) { - $inner_blocks = static::get_inner_blocks_from_navigation_post( $attributes ); - } - - // If there are no inner blocks then fallback to rendering an appropriate fallback. - if ( empty( $inner_blocks ) ) { - $inner_blocks = static::get_inner_blocks_from_fallback( $attributes ); - } - - /** - * Filter navigation block $inner_blocks. - * Allows modification of a navigation block menu items. - * - * @since 6.1.0 - * - * @param \WP_Block_List $inner_blocks - */ - $inner_blocks = apply_filters( 'block_core_navigation_render_inner_blocks', $inner_blocks ); - - $post_ids = gutenberg_block_core_navigation_get_post_ids( $inner_blocks ); - if ( $post_ids ) { - _prime_post_caches( $post_ids, false, false ); - } - - return $inner_blocks; - } - - /** - * Gets the name of the current navigation, if it has one. - * - * @param array $attributes The block attributes. - * @return string Returns the name of the navigation. - */ - private static function get_navigation_name( $attributes ) { - - $navigation_name = $attributes['ariaLabel'] ?? ''; - - // Load the navigation post. - if ( array_key_exists( 'ref', $attributes ) ) { - $navigation_post = get_post( $attributes['ref'] ); - if ( ! isset( $navigation_post ) ) { - return $navigation_name; - } - - // Only published posts are valid. If this is changed then a corresponding change - // must also be implemented in `use-navigation-menu.js`. - if ( 'publish' === $navigation_post->post_status ) { - $navigation_name = $navigation_post->post_title; - - // This is used to count the number of times a navigation name has been seen, - // so that we can ensure every navigation has a unique id. - if ( isset( static::$seen_menu_names[ $navigation_name ] ) ) { - ++static::$seen_menu_names[ $navigation_name ]; - } else { - static::$seen_menu_names[ $navigation_name ] = 1; - } - } - } - - return $navigation_name; - } - - /** - * Returns the layout class for the navigation block. - * - * @param array $attributes The block attributes. - * @return string Returns the layout class for the navigation block. - */ - private static function get_layout_class( $attributes ) { - $layout_justification = array( - 'left' => 'items-justified-left', - 'right' => 'items-justified-right', - 'center' => 'items-justified-center', - 'space-between' => 'items-justified-space-between', - ); - - $layout_class = ''; - if ( - isset( $attributes['layout']['justifyContent'] ) && - isset( $layout_justification[ $attributes['layout']['justifyContent'] ] ) - ) { - $layout_class .= $layout_justification[ $attributes['layout']['justifyContent'] ]; - } - if ( isset( $attributes['layout']['orientation'] ) && 'vertical' === $attributes['layout']['orientation'] ) { - $layout_class .= ' is-vertical'; - } - - if ( isset( $attributes['layout']['flexWrap'] ) && 'nowrap' === $attributes['layout']['flexWrap'] ) { - $layout_class .= ' no-wrap'; - } - return $layout_class; - } - - /** - * Return classes for the navigation block. - * - * @param array $attributes The block attributes. - * @return string Returns the classes for the navigation block. - */ - private static function get_classes( $attributes ) { - // Restore legacy classnames for submenu positioning. - $layout_class = static::get_layout_class( $attributes ); - $colors = gutenberg_block_core_navigation_build_css_colors( $attributes ); - $font_sizes = gutenberg_block_core_navigation_build_css_font_sizes( $attributes ); - $is_responsive_menu = static::is_responsive( $attributes ); - - // Manually add block support text decoration as CSS class. - $text_decoration = $attributes['style']['typography']['textDecoration'] ?? null; - $text_decoration_class = sprintf( 'has-text-decoration-%s', $text_decoration ); - - // Sets the is-collapsed class when the navigation is set to always use the overlay. - // This saves us from needing to do this check in the view.js file (see the collapseNav function). - $is_collapsed_class = static::is_always_overlay( $attributes ) ? array( 'is-collapsed' ) : array(); - - $classes = array_merge( - $colors['css_classes'], - $font_sizes['css_classes'], - $is_responsive_menu ? array( 'is-responsive' ) : array(), - $layout_class ? array( $layout_class ) : array(), - $text_decoration ? array( $text_decoration_class ) : array(), - $is_collapsed_class - ); - return implode( ' ', $classes ); - } - - private static function is_always_overlay( $attributes ) { - return isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu']; - } - - /** - * Get styles for the navigation block. - * - * @param array $attributes The block attributes. - * @return string Returns the styles for the navigation block. - */ - private static function get_styles( $attributes ) { - $colors = gutenberg_block_core_navigation_build_css_colors( $attributes ); - $font_sizes = gutenberg_block_core_navigation_build_css_font_sizes( $attributes ); - $block_styles = isset( $attributes['styles'] ) ? $attributes['styles'] : ''; - return $block_styles . $colors['inline_styles'] . $font_sizes['inline_styles']; - } - - /** - * Get the responsive container markup - * - * @param array $attributes The block attributes. - * @param WP_Block_List $inner_blocks The list of inner blocks. - * @param string $inner_blocks_html The markup for the inner blocks. - * @return string Returns the container markup. - */ - private static function get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ) { - $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); - $colors = gutenberg_block_core_navigation_build_css_colors( $attributes ); - $modal_unique_id = wp_unique_id( 'modal-' ); - - $responsive_container_classes = array( - 'wp-block-navigation__responsive-container', - implode( ' ', $colors['overlay_css_classes'] ), - ); - $open_button_classes = array( - 'wp-block-navigation__responsive-container-open', - ); - - $should_display_icon_label = isset( $attributes['hasIcon'] ) && true === $attributes['hasIcon']; - $toggle_button_icon = ''; - if ( isset( $attributes['icon'] ) ) { - if ( 'menu' === $attributes['icon'] ) { - $toggle_button_icon = ''; - } - } - $toggle_button_content = $should_display_icon_label ? $toggle_button_icon : __( 'Menu' ); - $toggle_close_button_icon = ''; - $toggle_close_button_content = $should_display_icon_label ? $toggle_close_button_icon : __( 'Close' ); - $toggle_aria_label_open = $should_display_icon_label ? 'aria-label="' . __( 'Open menu' ) . '"' : ''; // Open button label. - $toggle_aria_label_close = $should_display_icon_label ? 'aria-label="' . __( 'Close menu' ) . '"' : ''; // Close button label. - - // Add Interactivity API directives to the markup if needed. - $open_button_directives = ''; - $responsive_container_directives = ''; - $responsive_dialog_directives = ''; - $close_button_directives = ''; - if ( $should_load_view_script ) { - $open_button_directives = ' - data-wp-on--click="actions.openMenuOnClick" - data-wp-on--keydown="actions.handleMenuKeydown" - '; - $responsive_container_directives = ' - data-wp-class--has-modal-open="state.isMenuOpen" - data-wp-class--is-menu-open="state.isMenuOpen" - data-wp-watch="callbacks.initMenu" - data-wp-on--keydown="actions.handleMenuKeydown" - data-wp-on--focusout="actions.handleMenuFocusout" - tabindex="-1" - '; - $responsive_dialog_directives = ' - data-wp-bind--aria-modal="state.ariaModal" - data-wp-bind--aria-label="state.ariaLabel" - data-wp-bind--role="state.roleAttribute" - '; - $close_button_directives = ' - data-wp-on--click="actions.closeMenuOnClick" - '; - $responsive_container_content_directives = ' - data-wp-watch="callbacks.focusFirstElement" - '; - } - - return sprintf( - ' -
-
-
- -
- %2$s -
-
-
-
', - esc_attr( $modal_unique_id ), - $inner_blocks_html, - $toggle_aria_label_open, - $toggle_aria_label_close, - esc_attr( implode( ' ', $responsive_container_classes ) ), - esc_attr( implode( ' ', $open_button_classes ) ), - esc_attr( safecss_filter_attr( $colors['overlay_inline_styles'] ) ), - $toggle_button_content, - $toggle_close_button_content, - $open_button_directives, - $responsive_container_directives, - $responsive_dialog_directives, - $close_button_directives, - $responsive_container_content_directives - ); - } - - /** - * Get the wrapper attributes - * - * @param array $attributes The block attributes. - * @param WP_Block_List $inner_blocks A list of inner blocks. - * @return string Returns the navigation block markup. - */ - private static function get_nav_wrapper_attributes( $attributes, $inner_blocks ) { - $nav_menu_name = static::get_unique_navigation_name( $attributes ); - $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); - $is_responsive_menu = static::is_responsive( $attributes ); - $style = static::get_styles( $attributes ); - $class = static::get_classes( $attributes ); - $wrapper_attributes = get_block_wrapper_attributes( - array( - 'class' => $class, - 'style' => $style, - 'aria-label' => $nav_menu_name, - ) - ); - - if ( $is_responsive_menu ) { - $nav_element_directives = static::get_nav_element_directives( $should_load_view_script, $attributes ); - $wrapper_attributes .= ' ' . $nav_element_directives; - } - - return $wrapper_attributes; - } - - /** - * Get the nav element directives - * - * @param bool $should_load_view_script Whether or not the view script should be loaded. - * @return string the directives for the navigation element. - */ - private static function get_nav_element_directives( $should_load_view_script, $attributes ) { - if ( ! $should_load_view_script ) { - return ''; - } - // When adding to this array be mindful of security concerns. - $nav_element_context = wp_json_encode( - array( - 'overlayOpenedBy' => array(), - 'type' => 'overlay', - 'roleAttribute' => '', - 'ariaLabel' => __( 'Menu' ), - ), - JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP - ); - $nav_element_directives = ' - data-wp-interactive=\'{"namespace":"core/navigation"}\' - data-wp-context=\'' . $nav_element_context . '\' - '; - - // When the navigation overlayMenu attribute is set to "always" - // we don't need to use JavaScript to collapse the menu as we set the class manually. - if ( ! static::is_always_overlay( $attributes ) ) { - $nav_element_directives .= 'data-wp-init="callbacks.initNav"'; - $nav_element_directives .= ' '; // space separator - $nav_element_directives .= 'data-wp-class--is-collapsed="context.isCollapsed"'; - } - - return $nav_element_directives; - } - - /** - * Handle view script loading. - * - * @param array $attributes The block attributes. - * @param WP_Block $block The parsed block. - * @param WP_Block_List $inner_blocks The list of inner blocks. - */ - private static function handle_view_script_loading( $attributes, $block, $inner_blocks ) { - $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); - $is_gutenberg_plugin = defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN; - $view_js_file = 'wp-block-navigation-view'; - $script_handles = $block->block_type->view_script_handles; - - if ( $is_gutenberg_plugin ) { - if ( $should_load_view_script ) { - gutenberg_enqueue_module( '@wordpress/block-library/navigation-block' ); - } - // Remove the view script because we are using the module. - $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); - } else { - // If the script already exists, there is no point in removing it from viewScript. - if ( ! wp_script_is( $view_js_file ) ) { - - // If the script is not needed, and it is still in the `view_script_handles`, remove it. - if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); - } - // If the script is needed, but it was previously removed, add it again. - if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file ) ); - } - } - } - } - - /** - * Returns the markup for the navigation block. - * - * @param array $attributes The block attributes. - * @param WP_Block_List $inner_blocks The list of inner blocks. - * @return string Returns the navigation wrapper markup. - */ - private static function get_wrapper_markup( $attributes, $inner_blocks ) { - $inner_blocks_html = static::get_inner_blocks_html( $attributes, $inner_blocks ); - if ( static::is_responsive( $attributes ) ) { - return static::get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ); - } - return $inner_blocks_html; - } - - /** - * Returns a unique name for the navigation. - * - * @param array $attributes The block attributes. - * @return string Returns a unique name for the navigation. - */ - private static function get_unique_navigation_name( $attributes ) { - $nav_menu_name = static::get_navigation_name( $attributes ); - - // If the menu name has been used previously then append an ID - // to the name to ensure uniqueness across a given post. - if ( isset( static::$seen_menu_names[ $nav_menu_name ] ) && static::$seen_menu_names[ $nav_menu_name ] > 1 ) { - $count = static::$seen_menu_names[ $nav_menu_name ]; - $nav_menu_name = $nav_menu_name . ' ' . ( $count ); - } - - return $nav_menu_name; - } - - /** - * Renders the navigation block. - * - * @param array $attributes The block attributes. - * @param string $content The saved content. - * @param WP_Block $block The parsed block. - * @return string Returns the navigation block markup. - */ - public static function render( $attributes, $content, $block ) { - /** - * Deprecated: - * The rgbTextColor and rgbBackgroundColor attributes - * have been deprecated in favor of - * customTextColor and customBackgroundColor ones. - * Move the values from old attrs to the new ones. - */ - if ( isset( $attributes['rgbTextColor'] ) && empty( $attributes['textColor'] ) ) { - $attributes['customTextColor'] = $attributes['rgbTextColor']; - } - - if ( isset( $attributes['rgbBackgroundColor'] ) && empty( $attributes['backgroundColor'] ) ) { - $attributes['customBackgroundColor'] = $attributes['rgbBackgroundColor']; - } - - unset( $attributes['rgbTextColor'], $attributes['rgbBackgroundColor'] ); - - $inner_blocks = static::get_inner_blocks( $attributes, $block ); - // Prevent navigation blocks referencing themselves from rendering. - if ( gutenberg_block_core_navigation_block_contains_core_navigation( $inner_blocks ) ) { - return ''; - } - - static::handle_view_script_loading( $attributes, $block, $inner_blocks ); - - return sprintf( - '', - static::get_nav_wrapper_attributes( $attributes, $inner_blocks ), - static::get_wrapper_markup( $attributes, $inner_blocks ) - ); - } -} diff --git a/lib/compat/wordpress-6.5/class-wp-script-modules.php b/lib/compat/wordpress-6.5/class-wp-script-modules.php new file mode 100644 index 00000000000000..f6a2a348f92ef3 --- /dev/null +++ b/lib/compat/wordpress-6.5/class-wp-script-modules.php @@ -0,0 +1,323 @@ + + */ + private $enqueued_before_registered = array(); + + /** + * Registers the script module if no script module with that script module + * identifier has already been registered. + * + * @since 6.5.0 + * + * @param string $id The identifier of the script module. Should be unique. It will be used in the + * final import map. + * @param string $src Optional. Full URL of the script module, or path of the script module relative + * to the WordPress root directory. If it is provided and the script module has + * not been registered yet, it will be registered. + * @param array $deps { + * Optional. List of dependencies. + * + * @type string|array $0... { + * An array of script module identifiers of the dependencies of this script + * module. The dependencies can be strings or arrays. If they are arrays, + * they need an `id` key with the script module identifier, and can contain + * an `import` key with either `static` or `dynamic`. By default, + * dependencies that don't contain an `import` key are considered static. + * + * @type string $id The script module identifier. + * @type string $import Optional. Import type. May be either `static` or + * `dynamic`. Defaults to `static`. + * } + * } + * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false. + * It is added to the URL as a query string for cache busting purposes. If $version + * is set to false, the version number is the currently installed WordPress version. + * If $version is set to null, no version is added. + */ + public function register( string $id, string $src, array $deps = array(), $version = false ) { + if ( ! isset( $this->registered[ $id ] ) ) { + $dependencies = array(); + foreach ( $deps as $dependency ) { + if ( is_array( $dependency ) ) { + if ( ! isset( $dependency['id'] ) ) { + _doing_it_wrong( __METHOD__, __( 'Missing required id key in entry among dependencies array.' ), '6.5.0' ); + continue; + } + $dependencies[] = array( + 'id' => $dependency['id'], + 'import' => isset( $dependency['import'] ) && 'dynamic' === $dependency['import'] ? 'dynamic' : 'static', + ); + } elseif ( is_string( $dependency ) ) { + $dependencies[] = array( + 'id' => $dependency, + 'import' => 'static', + ); + } else { + _doing_it_wrong( __METHOD__, __( 'Entries in dependencies array must be either strings or arrays with an id key.' ), '6.5.0' ); + } + } + + $this->registered[ $id ] = array( + 'src' => $src, + 'version' => $version, + 'enqueue' => isset( $this->enqueued_before_registered[ $id ] ), + 'dependencies' => $dependencies, + ); + } + } + + /** + * Marks the script module to be enqueued in the page. + * + * If a src is provided and the script module has not been registered yet, it + * will be registered. + * + * @since 6.5.0 + * + * @param string $id The identifier of the script module. Should be unique. It will be used in the + * final import map. + * @param string $src Optional. Full URL of the script module, or path of the script module relative + * to the WordPress root directory. If it is provided and the script module has + * not been registered yet, it will be registered. + * @param array $deps { + * Optional. List of dependencies. + * + * @type string|array $0... { + * An array of script module identifiers of the dependencies of this script + * module. The dependencies can be strings or arrays. If they are arrays, + * they need an `id` key with the script module identifier, and can contain + * an `import` key with either `static` or `dynamic`. By default, + * dependencies that don't contain an `import` key are considered static. + * + * @type string $id The script module identifier. + * @type string $import Optional. Import type. May be either `static` or + * `dynamic`. Defaults to `static`. + * } + * } + * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false. + * It is added to the URL as a query string for cache busting purposes. If $version + * is set to false, the version number is the currently installed WordPress version. + * If $version is set to null, no version is added. + */ + public function enqueue( string $id, string $src = '', array $deps = array(), $version = false ) { + if ( isset( $this->registered[ $id ] ) ) { + $this->registered[ $id ]['enqueue'] = true; + } elseif ( $src ) { + $this->register( $id, $src, $deps, $version ); + $this->registered[ $id ]['enqueue'] = true; + } else { + $this->enqueued_before_registered[ $id ] = true; + } + } + + /** + * Unmarks the script module so it will no longer be enqueued in the page. + * + * @since 6.5.0 + * + * @param string $id The identifier of the script module. + */ + public function dequeue( string $id ) { + if ( isset( $this->registered[ $id ] ) ) { + $this->registered[ $id ]['enqueue'] = false; + } + unset( $this->enqueued_before_registered[ $id ] ); + } + + /** + * Adds the hooks to print the import map, enqueued script modules and script + * module preloads. + * + * In classic themes, the script modules used by the blocks are not yet known + * when the `wp_head` actions is fired, so it needs to print everything in the + * footer. + * + * @since 6.5.0 + */ + public function add_hooks() { + $position = wp_is_block_theme() ? 'wp_head' : 'wp_footer'; + add_action( $position, array( $this, 'print_import_map' ) ); + add_action( $position, array( $this, 'print_enqueued_script_modules' ) ); + add_action( $position, array( $this, 'print_script_module_preloads' ) ); + } + + /** + * Prints the enqueued script modules using script tags with type="module" + * attributes. + * + * @since 6.5.0 + */ + public function print_enqueued_script_modules() { + foreach ( $this->get_marked_for_enqueue() as $id => $script_module ) { + wp_print_script_tag( + array( + 'type' => 'module', + 'src' => $this->get_versioned_src( $script_module ), + 'id' => $id . '-js-module', + ) + ); + } + } + + /** + * Prints the the static dependencies of the enqueued script modules using + * link tags with rel="modulepreload" attributes. + * + * If a script module is marked for enqueue, it will not be preloaded. + * + * @since 6.5.0 + */ + public function print_script_module_preloads() { + foreach ( $this->get_dependencies( array_keys( $this->get_marked_for_enqueue() ), array( 'static' ) ) as $id => $script_module ) { + // Don't preload if it's marked for enqueue. + if ( true !== $script_module['enqueue'] ) { + echo sprintf( + '', + esc_url( $this->get_versioned_src( $script_module ) ), + esc_attr( $id . '-js-modulepreload' ) + ); + } + } + } + + /** + * Prints the import map using a script tag with a type="importmap" attribute. + * + * @since 6.5.0 + */ + public function print_import_map() { + $import_map = $this->get_import_map(); + if ( ! empty( $import_map['imports'] ) ) { + wp_print_inline_script_tag( + wp_json_encode( $import_map, JSON_HEX_TAG | JSON_HEX_AMP ), + array( + 'type' => 'importmap', + 'id' => 'wp-importmap', + ) + ); + } + } + + /** + * Returns the import map array. + * + * @since 6.5.0 + * + * @return array Array with an `imports` key mapping to an array of script module identifiers and their respective + * URLs, including the version query. + */ + private function get_import_map(): array { + $imports = array(); + foreach ( $this->get_dependencies( array_keys( $this->get_marked_for_enqueue() ) ) as $id => $script_module ) { + $imports[ $id ] = $this->get_versioned_src( $script_module ); + } + return array( 'imports' => $imports ); + } + + /** + * Retrieves the list of script modules marked for enqueue. + * + * @since 6.5.0 + * + * @return array Script modules marked for enqueue, keyed by script module identifier. + */ + private function get_marked_for_enqueue(): array { + $enqueued = array(); + foreach ( $this->registered as $id => $script_module ) { + if ( true === $script_module['enqueue'] ) { + $enqueued[ $id ] = $script_module; + } + } + return $enqueued; + } + + /** + * Retrieves all the dependencies for the given script module identifiers, + * filtered by import types. + * + * It will consolidate an array containing a set of unique dependencies based + * on the requested import types: 'static', 'dynamic', or both. This method is + * recursive and also retrieves dependencies of the dependencies. + * + * @since 6.5.0 + * + + * @param string[] $ids The identifiers of the script modules for which to gather dependencies. + * @param array $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both. + * Default is both. + * @return array List of dependencies, keyed by script module identifier. + */ + private function get_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ) { + return array_reduce( + $ids, + function ( $dependency_script_modules, $id ) use ( $import_types ) { + $dependencies = array(); + foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) { + if ( + in_array( $dependency['import'], $import_types, true ) && + isset( $this->registered[ $dependency['id'] ] ) && + ! isset( $dependency_script_modules[ $dependency['id'] ] ) + ) { + $dependencies[ $dependency['id'] ] = $this->registered[ $dependency['id'] ]; + } + } + return array_merge( $dependency_script_modules, $dependencies, $this->get_dependencies( array_keys( $dependencies ), $import_types ) ); + }, + array() + ); + } + + /** + * Gets the versioned URL for a script module src. + * + * If $version is set to false, the version number is the currently installed + * WordPress version. If $version is set to null, no version is added. + * Otherwise, the string passed in $version is used. + * + * @since 6.5.0 + * + * @param array $script_module The script module. + * @return string The script module src with a version if relevant. + */ + private function get_versioned_src( array $script_module ): string { + $args = array(); + if ( false === $script_module['version'] ) { + $args['ver'] = get_bloginfo( 'version' ); + } elseif ( null !== $script_module['version'] ) { + $args['ver'] = $script_module['version']; + } + if ( $args ) { + return add_query_arg( $args, $script_module['src'] ); + } + return $script_module['src']; + } + } +} diff --git a/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-open-elements-6-5.php b/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-open-elements-6-5.php new file mode 100644 index 00000000000000..0d8901225c792e --- /dev/null +++ b/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-open-elements-6-5.php @@ -0,0 +1,462 @@ + 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_5 { + /** + * 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; + + /** + * 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; + } + + $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 ) { + $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 ) { + 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; + } + } + + /** + * 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; + } + } +} diff --git a/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-processor-6-5.php b/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-processor-6-5.php new file mode 100644 index 00000000000000..eae2b1eda815ca --- /dev/null +++ b/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-processor-6-5.php @@ -0,0 +1,1745 @@ +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, LABEL, LEGEND, METER, PROGRESS, SEARCH. + * - Formatting elements: B, BIG, CODE, EM, FONT, I, SMALL, STRIKE, STRONG, TT, U. + * - Heading elements: H1, H2, H3, H4, H5, H6, HGROUP. + * - Links: A. + * - Lists: DD, DL, DT, LI, OL, LI. + * - Media elements: AUDIO, CANVAS, FIGCAPTION, FIGURE, IMG, MAP, PICTURE, VIDEO. + * - Paragraph: P. + * - Phrasing elements: ABBR, BDI, BDO, CITE, DATA, DEL, DFN, INS, MARK, OUTPUT, Q, SAMP, SUB, SUP, TIME, VAR. + * - Sectioning elements: ARTICLE, ASIDE, NAV, SECTION. + * - Templating elements: SLOT. + * - Text decoration: RUBY. + * - Deprecated elements: ACRONYM, BLINK, CENTER, DIR, ISINDEX, MULTICOL, NEXTID, 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_5 extends Gutenberg_HTML_Tag_Processor_6_5 { + /** + * 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; + + /** + * Static query for instructing the Tag Processor to visit every token. + * + * @access private + * + * @since 6.4.0 + * + * @var array + */ + const VISIT_EVERYTHING = array( 'tag_closers' => 'visit' ); + + /** + * 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; + + /* + * 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 + * + * @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 WP_HTML_Processor|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; + } + + $p = new self( $html, self::CONSTRUCTOR_UNLOCK_CODE ); + $p->state->context_node = array( 'BODY', array() ); + $p->state->insertion_mode = WP_HTML_Processor_State::INSERTION_MODE_IN_BODY; + + // @todo Create "fake" bookmarks for non-existent but implied nodes. + $p->bookmarks['root-node'] = new WP_HTML_Span( 0, 0 ); + $p->bookmarks['context-node'] = new WP_HTML_Span( 0, 0 ); + + $p->state->stack_of_open_elements->push( + new WP_HTML_Token( + 'root-node', + 'HTML', + false + ) + ); + + $p->state->stack_of_open_elements->push( + new WP_HTML_Token( + 'context-node', + $p->state->context_node[0], + false + ) + ); + + return $p; + } + + /** + * 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_5(); + + /* + * 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( '