diff --git a/.eslintrc.js b/.eslintrc.js
index a81369b61c5f1..fd7e9183781ea 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -294,7 +294,7 @@ module.exports = {
'packages/e2e-test-utils-playwright/**/*.[tj]s',
],
extends: [
- 'plugin:eslint-plugin-playwright/playwright-test',
+ 'plugin:@wordpress/eslint-plugin/test-playwright',
'plugin:@typescript-eslint/base',
],
parserOptions: {
@@ -308,7 +308,6 @@ module.exports = {
rules: {
'@wordpress/no-global-active-element': 'off',
'@wordpress/no-global-get-selection': 'off',
- 'playwright/no-page-pause': 'error',
'no-restricted-syntax': [
'error',
{
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 22404fc7c96eb..68326eaa23cae 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -135,6 +135,7 @@
# PHP
/lib @spacedmonkey
+/lib/compat/*/html-api @dmsnell
/lib/experimental/rest-api.php @timothybjacobs
/lib/experimental/class-wp-rest-* @timothybjacobs
/lib/experimental/class-wp-rest-block-editor-settings-controller.php @timothybjacobs @spacedmonkey @geriux
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index f6202ef81dea8..cdeffc9b14c88 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -10,5 +10,4 @@ updates:
open-pull-requests-limit: 10
labels:
- 'GitHub Actions'
- - 'Automated Testing'
- '[Type] Build Tooling'
diff --git a/.github/workflows/build-plugin-zip.yml b/.github/workflows/build-plugin-zip.yml
index e8a65e6966150..5c06393ec3c01 100644
--- a/.github/workflows/build-plugin-zip.yml
+++ b/.github/workflows/build-plugin-zip.yml
@@ -69,9 +69,10 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
with:
token: ${{ secrets.GUTENBERG_TOKEN }}
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Compute old and new version
id: get_version
@@ -164,12 +165,13 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
with:
ref: ${{ needs.bump-version.outputs.release_branch || github.ref }}
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Use desired version of Node.js
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
+ uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
with:
node-version-file: '.nvmrc'
cache: npm
@@ -180,7 +182,7 @@ jobs:
NO_CHECKS: 'true'
- name: Upload artifact
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
+ uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: gutenberg-plugin
path: ./gutenberg.zip
@@ -203,7 +205,7 @@ jobs:
- name: Upload release notes artifact
if: ${{ needs.bump-version.outputs.new_version }}
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
+ uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: release-notes
path: ./release-notes.txt
@@ -219,11 +221,12 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
with:
fetch-depth: 2
ref: ${{ needs.bump-version.outputs.release_branch }}
token: ${{ secrets.GUTENBERG_TOKEN }}
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Configure git user name and email
run: |
@@ -307,18 +310,20 @@ jobs:
if: ${{ endsWith( needs.bump-version.outputs.new_version, '-rc.1' ) }}
steps:
- name: Checkout (for CLI)
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
with:
path: main
ref: trunk
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Checkout (for publishing)
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
with:
path: publish
# Later, we switch this branch in the script that publishes packages.
ref: trunk
token: ${{ secrets.GUTENBERG_TOKEN }}
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Configure git user name and email (for publishing)
run: |
@@ -327,7 +332,7 @@ jobs:
git config user.email gutenberg@wordpress.org
- name: Setup Node.js
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
+ uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
with:
node-version-file: 'main/.nvmrc'
registry-url: 'https://registry.npmjs.org'
diff --git a/.github/workflows/bundle-size.yml b/.github/workflows/bundle-size.yml
index c02731c057537..f5a28f7859097 100644
--- a/.github/workflows/bundle-size.yml
+++ b/.github/workflows/bundle-size.yml
@@ -37,12 +37,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
with:
fetch-depth: 1
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Use desired version of Node.js
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
+ uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
with:
node-version-file: '.nvmrc'
cache: npm
diff --git a/.github/workflows/check-components-changelog.yml b/.github/workflows/check-components-changelog.yml
index 26d14febcc41e..5271b3883c141 100644
--- a/.github/workflows/check-components-changelog.yml
+++ b/.github/workflows/check-components-changelog.yml
@@ -20,11 +20,12 @@ jobs:
- name: 'Get PR commit count'
run: echo "PR_COMMIT_COUNT=$(( ${{ github.event.pull_request.commits }} + 1 ))" >> "${GITHUB_ENV}"
- name: Checkout code
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: ${{ env.PR_COMMIT_COUNT }}
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: 'Fetch relevant history from origin'
run: git fetch origin ${{ github.event.pull_request.base.ref }}
- name: Check CHANGELOG status
diff --git a/.github/workflows/create-block.yml b/.github/workflows/create-block.yml
index d3b0efa0ce5d9..f3c9f64b97072 100644
--- a/.github/workflows/create-block.yml
+++ b/.github/workflows/create-block.yml
@@ -24,7 +24,9 @@ jobs:
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+ with:
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Setup Node.js and install dependencies
uses: ./.github/setup-node
diff --git a/.github/workflows/end2end-test.yml b/.github/workflows/end2end-test.yml
index 157d670c09913..61cb39612fa55 100644
--- a/.github/workflows/end2end-test.yml
+++ b/.github/workflows/end2end-test.yml
@@ -27,7 +27,9 @@ jobs:
totalParts: [3]
steps:
- - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+ with:
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Setup Node.js and install dependencies
uses: ./.github/setup-node
@@ -45,7 +47,7 @@ jobs:
npx wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % ${{ matrix.totalParts }} == ${{ matrix.part }} - 1' < ~/.jest-e2e-tests )
- name: Archive debug artifacts (screenshots, HTML snapshots)
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
+ uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
if: always()
with:
name: failures-artifacts
@@ -53,7 +55,7 @@ jobs:
if-no-files-found: ignore
- name: Archive flaky tests report
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
+ uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
if: always()
with:
name: flaky-tests-report
@@ -71,7 +73,9 @@ jobs:
totalParts: [4]
steps:
- - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+ with:
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Setup Node.js and install dependencies
uses: ./.github/setup-node
@@ -92,7 +96,7 @@ jobs:
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test:e2e:playwright -- --shard=${{ matrix.part }}/${{ matrix.totalParts }}
- name: Archive debug artifacts (screenshots, traces)
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
+ uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
if: always()
with:
name: failures-artifacts
@@ -100,7 +104,7 @@ jobs:
if-no-files-found: ignore
- name: Archive flaky tests report
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
+ uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
if: always()
with:
name: flaky-tests-report
@@ -115,9 +119,10 @@ jobs:
steps:
# Checkout defaults to using the branch which triggered the event, which
# isn't necessarily `trunk` (e.g. in the case of a merge).
- - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
with:
ref: trunk
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- uses: actions/download-artifact@v3
id: download_artifact
diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml
index 633ea7135acc4..5e417c27bc82f 100644
--- a/.github/workflows/gradle-wrapper-validation.yml
+++ b/.github/workflows/gradle-wrapper-validation.yml
@@ -6,5 +6,7 @@ jobs:
name: 'Validation'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+ with:
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- uses: gradle/wrapper-validation-action@v1
diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml
index 5c4c7b068108f..7813f04874bc2 100644
--- a/.github/workflows/performance.yml
+++ b/.github/workflows/performance.yml
@@ -32,7 +32,9 @@ jobs:
WP_ARTIFACTS_PATH: ${{ github.workspace }}/artifacts
steps:
- - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+ with:
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Setup Node.js and install dependencies
uses: ./.github/setup-node
@@ -66,13 +68,13 @@ jobs:
- name: Compare performance with base branch
if: github.event_name == 'push'
# The base hash used here need to be a commit that is compatible with the current WP version
- # The current one is 34af5829ac9edb31833167ff6a3b51bea982999c and it needs to be updated every WP major release.
+ # The current one is bd2a881101727b03b0be09382b34841af5a3c03e and it needs to be updated every WP major release.
# It is used as a base comparison point to avoid fluctuation in the performance metrics.
run: |
WP_VERSION=$(awk -F ': ' '/^Tested up to/{print $2}' readme.txt)
IFS=. read -ra WP_VERSION_ARRAY <<< "$WP_VERSION"
WP_MAJOR="${WP_VERSION_ARRAY[0]}.${WP_VERSION_ARRAY[1]}"
- ./bin/plugin/cli.js perf $GITHUB_SHA 34af5829ac9edb31833167ff6a3b51bea982999c --tests-branch $GITHUB_SHA --wp-version "$WP_MAJOR"
+ ./bin/plugin/cli.js perf $GITHUB_SHA bd2a881101727b03b0be09382b34841af5a3c03e --tests-branch $GITHUB_SHA --wp-version "$WP_MAJOR"
- name: Compare performance with custom branches
if: github.event_name == 'workflow_dispatch'
@@ -84,7 +86,7 @@ jobs:
- name: Archive performance results
if: success()
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
+ uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: performance-results
path: ${{ env.WP_ARTIFACTS_PATH }}/*.performance-results*.json
@@ -95,10 +97,10 @@ jobs:
CODEHEALTH_PROJECT_TOKEN: ${{ secrets.CODEHEALTH_PROJECT_TOKEN }}
run: |
COMMITTED_AT=$(git show -s $GITHUB_SHA --format="%cI")
- ./bin/log-performance-results.js $CODEHEALTH_PROJECT_TOKEN trunk $GITHUB_SHA 34af5829ac9edb31833167ff6a3b51bea982999c $COMMITTED_AT
+ ./bin/log-performance-results.js $CODEHEALTH_PROJECT_TOKEN trunk $GITHUB_SHA bd2a881101727b03b0be09382b34841af5a3c03e $COMMITTED_AT
- name: Archive debug artifacts (screenshots, HTML snapshots)
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
+ uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
if: failure()
with:
name: failures-artifacts
diff --git a/.github/workflows/php-changes-detection.yml b/.github/workflows/php-changes-detection.yml
index 2de5b57362281..189639c3f4764 100644
--- a/.github/workflows/php-changes-detection.yml
+++ b/.github/workflows/php-changes-detection.yml
@@ -10,13 +10,14 @@ jobs:
if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }}
steps:
- name: Check out code
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
with:
fetch-depth: 0
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Get changed PHP files
id: changed-files-php
- uses: tj-actions/changed-files@v37
+ uses: tj-actions/changed-files@48566bbcc22ceb7c5809ebdd27377309f2c3de8c # v39
with:
files: |
*.{php}
diff --git a/.github/workflows/publish-npm-packages.yml b/.github/workflows/publish-npm-packages.yml
index dd1f188e0ae2f..7c2e9412cc1de 100644
--- a/.github/workflows/publish-npm-packages.yml
+++ b/.github/workflows/publish-npm-packages.yml
@@ -30,18 +30,31 @@ jobs:
environment: WordPress packages
steps:
- name: Checkout (for CLI)
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ if: ${{ github.event.inputs.release_type != 'wp' }}
+ uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
with:
path: cli
ref: trunk
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Checkout (for publishing)
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ if: ${{ github.event.inputs.release_type != 'wp' }}
+ uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
with:
path: publish
# Later, we switch this branch in the script that publishes packages.
ref: trunk
token: ${{ secrets.GUTENBERG_TOKEN }}
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
+
+ - name: Checkout (for publishing WP major version)
+ if: ${{ github.event.inputs.release_type == 'wp' && github.event.inputs.wp_version }}
+ uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+ with:
+ path: publish
+ ref: wp/${{ github.event.inputs.wp_version }}
+ token: ${{ secrets.GUTENBERG_TOKEN }}
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Configure git user name and email (for publishing)
run: |
@@ -50,11 +63,19 @@ jobs:
git config user.email gutenberg@wordpress.org
- name: Setup Node.js
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
+ if: ${{ github.event.inputs.release_type != 'wp' }}
+ uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
with:
node-version-file: 'cli/.nvmrc'
registry-url: 'https://registry.npmjs.org'
+ - name: Setup Node.js (for WP major version)
+ if: ${{ github.event.inputs.release_type == 'wp' && github.event.inputs.wp_version }}
+ uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
+ with:
+ node-version-file: 'publish/.nvmrc'
+ registry-url: 'https://registry.npmjs.org'
+
- name: Publish development packages to npm ("next" dist-tag)
if: ${{ github.event.inputs.release_type == 'development' }}
run: |
@@ -73,7 +94,7 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- - name: Publish packages to npm for WP major ("wp/${{ github.event.inputs.wp_version || 'X.Y' }}" dist-tag)
+ - name: Publish packages to npm for WP major version ("wp/${{ github.event.inputs.wp_version || 'X.Y' }}" dist-tag)
if: ${{ github.event.inputs.release_type == 'wp' && github.event.inputs.wp_version }}
run: |
cd publish
diff --git a/.github/workflows/pull-request-automation.yml b/.github/workflows/pull-request-automation.yml
index 7bcfb9744aa22..a34df5282d6d8 100644
--- a/.github/workflows/pull-request-automation.yml
+++ b/.github/workflows/pull-request-automation.yml
@@ -15,17 +15,18 @@ jobs:
steps:
# Checkout defaults to using the branch which triggered the event, which
# isn't necessarily `trunk` (e.g. in the case of a merge).
- - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
with:
ref: trunk
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Use desired version of Node.js
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
+ uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
with:
node-version: ${{ matrix.node }}
- name: Cache NPM packages
- uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
+ uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
diff --git a/.github/workflows/rnmobile-android-runner.yml b/.github/workflows/rnmobile-android-runner.yml
index 7b0244b0ec505..1d4e010558a6e 100644
--- a/.github/workflows/rnmobile-android-runner.yml
+++ b/.github/workflows/rnmobile-android-runner.yml
@@ -23,7 +23,9 @@ jobs:
steps:
- name: checkout
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+ with:
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Use desired version of Java
uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0
@@ -35,10 +37,10 @@ jobs:
uses: ./.github/setup-node
- name: Gradle cache
- uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c # v2.7.0
+ uses: gradle/gradle-build-action@ef76a971e2fa3f867b617efd72f2fbd72cf6f8bc # v2.8.0
- name: AVD cache
- uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
+ uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2
id: avd-cache
with:
path: |
@@ -69,13 +71,13 @@ jobs:
profile: Nexus 6
script: npm run native test:e2e:android:local ${{ matrix.native-test-name }}
- - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
+ - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
if: always()
with:
name: android-screen-recordings
path: packages/react-native-editor/android-screen-recordings
- - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
+ - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
if: always()
with:
name: appium-logs
diff --git a/.github/workflows/rnmobile-ios-runner.yml b/.github/workflows/rnmobile-ios-runner.yml
index f7ee1821f3650..6a03547966fe1 100644
--- a/.github/workflows/rnmobile-ios-runner.yml
+++ b/.github/workflows/rnmobile-ios-runner.yml
@@ -23,7 +23,9 @@ jobs:
native-test-name: [gutenberg-editor-rendering]
steps:
- - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+ with:
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Setup Node.js and install dependencies
uses: ./.github/setup-node
@@ -32,7 +34,7 @@ jobs:
run: find package-lock.json packages/react-native-editor/ios packages/react-native-aztec/ios packages/react-native-bridge/ios -type f -print0 | sort -z | xargs -0 shasum | tee ios-checksums.txt
- name: Restore build cache
- uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
+ uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2
with:
path: |
packages/react-native-editor/ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app
@@ -40,7 +42,7 @@ jobs:
key: ${{ runner.os }}-ios-build-${{ matrix.xcode }}-${{ matrix.device }}-${{ hashFiles('ios-checksums.txt') }}
- name: Restore pods cache
- uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
+ uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2
with:
path: |
packages/react-native-editor/ios/Pods
@@ -72,13 +74,13 @@ jobs:
rm packages/react-native-editor/ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app/main.jsbundle
rm -rf packages/react-native-editor/ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app/assets
- - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
+ - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
if: always()
with:
name: ios-screen-recordings
path: packages/react-native-editor/ios-screen-recordings
- - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
+ - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
if: always()
with:
name: appium-logs
diff --git a/.github/workflows/static-checks.yml b/.github/workflows/static-checks.yml
index 72b975522df82..87e936757814e 100644
--- a/.github/workflows/static-checks.yml
+++ b/.github/workflows/static-checks.yml
@@ -22,10 +22,12 @@ jobs:
if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }}
steps:
- - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+ with:
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Use desired version of Node.js
- uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
+ uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
with:
node-version-file: '.nvmrc'
cache: npm
diff --git a/.github/workflows/storybook-pages.yml b/.github/workflows/storybook-pages.yml
index 32c26652b3de0..d8932b4a4e277 100644
--- a/.github/workflows/storybook-pages.yml
+++ b/.github/workflows/storybook-pages.yml
@@ -12,9 +12,10 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
with:
ref: trunk
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Setup Node.js and install dependencies
uses: ./.github/setup-node
diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml
index 693543a81958f..fd021b1b33815 100644
--- a/.github/workflows/unit-test.yml
+++ b/.github/workflows/unit-test.yml
@@ -31,7 +31,9 @@ jobs:
node: ['16']
steps:
- - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+ with:
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Setup Node.js and install dependencies
uses: ./.github/setup-node
@@ -109,7 +111,9 @@ jobs:
WP_ENV_CORE: ${{ matrix.wordpress == '' && 'WordPress/WordPress' || format( 'https://wordpress.org/wordpress-{0}.zip', matrix.wordpress ) }}
steps:
- - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+ with:
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Setup Node.js and install dependencies
uses: ./.github/setup-node
@@ -122,7 +126,7 @@ jobs:
# dependency versions are installed and cached.
##
- name: Set up PHP
- uses: shivammathur/setup-php@4bd44f22a98a19e0950cbad5f31095157cc9621b # v2.25.4
+ uses: shivammathur/setup-php@72ae4ccbe57f82bbe08411e84e2130bd4ba1c10f # v2.25.5
with:
php-version: '${{ matrix.php }}'
ini-file: development
@@ -217,10 +221,12 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+ with:
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Set up PHP
- uses: shivammathur/setup-php@4bd44f22a98a19e0950cbad5f31095157cc9621b # v2.25.4
+ uses: shivammathur/setup-php@72ae4ccbe57f82bbe08411e84e2130bd4ba1c10f # v2.25.5
with:
php-version: '7.4'
coverage: none
@@ -233,7 +239,7 @@ jobs:
run: echo "date=$(/bin/date -u --date='last Mon' "+%F")" >> $GITHUB_OUTPUT
- name: Cache PHPCS scan cache
- uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
+ uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2
with:
path: .cache/phpcs.json
key: ${{ runner.os }}-date-${{ steps.get-date.outputs.date }}-phpcs-cache-${{ hashFiles('**/composer.json', 'phpcs.xml.dist') }}
@@ -284,7 +290,9 @@ jobs:
if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }}
steps:
- - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+ with:
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Setup Node.js and install dependencies
uses: ./.github/setup-node
diff --git a/.github/workflows/upload-release-to-plugin-repo.yml b/.github/workflows/upload-release-to-plugin-repo.yml
index c39e8e6191b42..d7fdb9b3fe379 100644
--- a/.github/workflows/upload-release-to-plugin-repo.yml
+++ b/.github/workflows/upload-release-to-plugin-repo.yml
@@ -96,10 +96,11 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
with:
ref: ${{ matrix.branch }}
token: ${{ secrets.GUTENBERG_TOKEN }}
+ show-progress: ${{ runner.debug == '1' && 'true' || 'false' }}
- name: Update the Changelog to include the release notes
run: |
@@ -146,7 +147,7 @@ jobs:
fi
- name: Upload Changelog artifact
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
+ uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: changelog ${{ matrix.label }}
path: ./changelog.txt
diff --git a/.npmpackagejsonlintrc.json b/.npmpackagejsonlintrc.json
index 5c240bf5c3bf4..43c2e4552edbc 100644
--- a/.npmpackagejsonlintrc.json
+++ b/.npmpackagejsonlintrc.json
@@ -29,6 +29,14 @@
"require-repository-directory": "off",
"prefer-no-devDependencies": "off"
}
+ },
+ {
+ "patterns": [ "./platform-docs/package.json" ],
+ "rules": {
+ "require-publishConfig": "off",
+ "require-repository-directory": "off",
+ "prefer-no-devDependencies": "off"
+ }
}
]
}
diff --git a/bin/cherry-pick.mjs b/bin/cherry-pick.mjs
index baf8d42962f8e..91ded84489e68 100644
--- a/bin/cherry-pick.mjs
+++ b/bin/cherry-pick.mjs
@@ -118,19 +118,10 @@ async function fetchPRs() {
id,
number,
title,
- closed_at,
pull_request,
- } ) ).sort( ( a, b ) => {
- /*
- * `closed_at` and `pull_request.merged_at` are _usually_ the same,
- * but let's prefer the latter if it's available.
- */
- if ( a?.pull_request?.merged_at && b?.pull_request?.merged_at ) {
- return new Date( a?.pull_request?.merged_at ) - new Date( b?.pull_request?.merged_at );
- }
- return new Date( a.closed_at ) - new Date( b.closed_at );
- } );
-
+ } ) )
+ .filter( ( { pull_request } ) => !! 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): ' );
PRs.forEach( ( { number, title } ) =>
diff --git a/bin/packages/check-build-type-declaration-files.js b/bin/packages/check-build-type-declaration-files.js
index cd8bad2a70b53..fff0b51e32fc2 100644
--- a/bin/packages/check-build-type-declaration-files.js
+++ b/bin/packages/check-build-type-declaration-files.js
@@ -83,7 +83,7 @@ async function checkUnverifiedDeclarationFiles() {
const packageDir = path.resolve( 'packages' );
const packageDirs = (
await fs.readdir( packageDir, { withFileTypes: true } )
- )
+ )
.filter( ( dirent ) => dirent.isDirectory() )
.map( ( dirent ) => path.join( packageDir, dirent.name ) );
@@ -97,7 +97,7 @@ async function checkUnverifiedDeclarationFiles() {
: null
)
)
- ).filter( Boolean );
+ ).filter( Boolean );
const tscResults = await Promise.allSettled(
declarations.map( typecheckDeclarations )
diff --git a/bin/plugin/commands/changelog.js b/bin/plugin/commands/changelog.js
index 02cb5f7afdd81..59d77c3fe9610 100644
--- a/bin/plugin/commands/changelog.js
+++ b/bin/plugin/commands/changelog.js
@@ -72,7 +72,7 @@ const LABEL_TYPE_MAPPING = {
'[Type] Project Management': 'Tools',
'[Package] Scripts': 'Tools',
'[Type] Build Tooling': 'Tools',
- 'Automated Testing': 'Tools',
+ '[Type] Automated Testing': 'Tools',
'[Package] Dependency Extraction Webpack Plugin': 'Tools',
'[Type] Code Quality': 'Code Quality',
'[Type] Accessibility (a11y)': 'Accessibility',
@@ -128,7 +128,7 @@ const LABEL_FEATURE_MAPPING = {
'New Block': 'Block Library',
'[Package] E2E Tests': 'Testing',
'[Package] E2E Test Utils': 'Testing',
- 'Automated Testing': 'Testing',
+ '[Type] Automated Testing': 'Testing',
'CSS Styling': 'CSS & Styling',
'developer-docs': 'Documentation',
'[Type] Developer Documentation': 'Documentation',
diff --git a/bin/plugin/commands/packages.js b/bin/plugin/commands/packages.js
index 87ecc6eb89cfc..fb83060b6e924 100644
--- a/bin/plugin/commands/packages.js
+++ b/bin/plugin/commands/packages.js
@@ -150,6 +150,7 @@ async function runNpmReleaseBranchSyncStep( pluginReleaseBranch, config ) {
*/
await repo
.raw( 'rm', '-r', '.' )
+ .fetch( 'origin', pluginReleaseBranch, [ '--depth=1' ] )
.raw( 'checkout', `origin/${ pluginReleaseBranch }`, '--', '.' );
const { commit: commitHash } = await repo.commit(
@@ -197,10 +198,9 @@ async function updatePackages( config ) {
);
const changelogFilesPublicPackages = changelogFiles.filter(
( changelogPath ) => {
- const pkg = require( path.join(
- path.dirname( changelogPath ),
- 'package.json'
- ) );
+ const pkg = require(
+ path.join( path.dirname( changelogPath ), 'package.json' )
+ );
return pkg.private !== true;
}
);
diff --git a/bin/plugin/commands/performance.js b/bin/plugin/commands/performance.js
index e153f482137e5..4be675a0a5d40 100644
--- a/bin/plugin/commands/performance.js
+++ b/bin/plugin/commands/performance.js
@@ -1,6 +1,7 @@
/**
* External dependencies
*/
+const os = require( 'os' );
const fs = require( 'fs' );
const path = require( 'path' );
const SimpleGit = require( 'simple-git' );
@@ -13,7 +14,6 @@ const {
runShellScript,
readJSONFile,
askForConfirmation,
- getRandomTemporaryPath,
getFilesFromDir,
} = require( '../lib/utils' );
const config = require( '../config' );
@@ -31,6 +31,19 @@ const RESULTS_FILE_SUFFIX = '.performance-results.json';
* @property {string=} wpVersion The WordPress version to be used as the base install for testing.
*/
+/**
+ * A logging helper for printing steps and their substeps.
+ *
+ * @param {number} indent Value to indent the log.
+ * @param {any} msg Message to log.
+ * @param {...any} args Rest of the arguments to pass to console.log.
+ */
+function logAtIndent( indent, msg, ...args ) {
+ const prefix = indent === 0 ? 'ā¶ ' : '> ';
+ const newline = indent === 0 ? '\n' : '';
+ return log( newline + ' '.repeat( indent ) + prefix + msg, ...args );
+}
+
/**
* Sanitizes branch name to be used in a path or a filename.
*
@@ -64,14 +77,14 @@ function median( array ) {
/**
* Runs the performance tests on the current branch.
*
- * @param {string} testSuite Name of the tests set.
- * @param {string} performanceTestDirectory Path to the performance tests' clone.
- * @param {string} runKey Unique identifier for the test run.
+ * @param {string} testSuite Name of the tests set.
+ * @param {string} testRunnerDir Path to the performance tests' clone.
+ * @param {string} runKey Unique identifier for the test run.
*/
-async function runTestSuite( testSuite, performanceTestDirectory, runKey ) {
+async function runTestSuite( testSuite, testRunnerDir, runKey ) {
await runShellScript(
`npm run test:performance -- ${ testSuite }`,
- performanceTestDirectory,
+ testRunnerDir,
{
...process.env,
WP_ARTIFACTS_PATH: ARTIFACTS_PATH,
@@ -95,23 +108,23 @@ async function runPerformanceTests( branches, options ) {
branches = [ 'trunk' ];
}
+ log( formats.title( '\nš Performance Tests šŗ' ) );
log(
- formats.title( '\nš Performance Tests šŗ\n' ),
- '\nWelcome! This tool runs the performance tests on multiple branches and displays a comparison table.\n' +
- 'In order to run the tests, the tool is going to load a WordPress environment on ports 8888 and 8889.\n' +
- 'Make sure these ports are not used before continuing.\n'
+ '\nWelcome! This tool runs the performance tests on multiple branches and displays a comparison table.'
);
if ( ! runningInCI ) {
+ log(
+ formats.warning(
+ '\nIn order to run the tests, the tool is going to load a WordPress environment on ports 8888 and 8889.' +
+ '\nMake sure these ports are not used before continuing.\n'
+ )
+ );
+
await askForConfirmation( 'Ready to go? ' );
}
- /*
- * 1- Preparing the tests directory.
- */
-
- log( '\n>> Preparing the tests directories' );
- log( ' >> Cloning the repository' );
+ logAtIndent( 0, 'Setting up' );
/**
* @type {string[]} git refs against which to run tests;
@@ -121,121 +134,162 @@ async function runPerformanceTests( branches, options ) {
throw new Error( `Need at least two git refs to run` );
}
- const baseDirectory = getRandomTemporaryPath();
- fs.mkdirSync( baseDirectory, { recursive: true } );
+ const baseDir = path.join( os.tmpdir(), 'wp-performance-tests' );
+
+ if ( fs.existsSync( baseDir ) ) {
+ logAtIndent( 1, 'Removing existing files' );
+ fs.rmSync( baseDir, { recursive: true } );
+ }
+
+ logAtIndent( 1, 'Creating base directory:', formats.success( baseDir ) );
+ fs.mkdirSync( baseDir );
+
+ logAtIndent( 1, 'Setting up repository' );
+ const sourceDir = path.join( baseDir, 'source' );
+
+ logAtIndent( 2, 'Creating directory:', formats.success( sourceDir ) );
+ fs.mkdirSync( sourceDir );
// @ts-ignore
- const git = SimpleGit( baseDirectory );
- await git
+ const sourceGit = SimpleGit( sourceDir );
+ logAtIndent(
+ 2,
+ 'Initializing:',
+ formats.success( config.gitRepositoryURL )
+ );
+ await sourceGit
.raw( 'init' )
.raw( 'remote', 'add', 'origin', config.gitRepositoryURL );
- for ( const branch of branches ) {
- await git.raw( 'fetch', '--depth=1', 'origin', branch );
+ for ( const [ i, branch ] of branches.entries() ) {
+ logAtIndent(
+ 2,
+ `Fetching environment branch (${ i + 1 } of ${ branches.length }):`,
+ formats.success( branch )
+ );
+ await sourceGit.raw( 'fetch', '--depth=1', 'origin', branch );
}
- await git.raw( 'checkout', branches[ 0 ] );
+ const testRunnerBranch = options.testsBranch || branches[ 0 ];
+ if ( options.testsBranch && ! branches.includes( options.testsBranch ) ) {
+ logAtIndent(
+ 2,
+ 'Fetching test runner branch:',
+ formats.success( options.testsBranch )
+ );
+ // @ts-ignore
+ await sourceGit.raw(
+ 'fetch',
+ '--depth=1',
+ 'origin',
+ options.testsBranch
+ );
+ } else {
+ logAtIndent(
+ 2,
+ 'Using test runner branch:',
+ formats.success( testRunnerBranch )
+ );
+ }
- const rootDirectory = getRandomTemporaryPath();
- const performanceTestDirectory = rootDirectory + '/tests';
- await runShellScript( 'mkdir -p ' + rootDirectory );
- await runShellScript(
- 'cp -R ' + baseDirectory + ' ' + performanceTestDirectory
- );
+ logAtIndent( 1, 'Setting up test runner' );
- if ( !! options.testsBranch ) {
- const branchName = formats.success( options.testsBranch );
- log( ` >> Fetching the test-runner branch: ${ branchName }` );
+ const testRunnerDir = path.join( baseDir + '/tests' );
- // @ts-ignore
- await SimpleGit( performanceTestDirectory )
- .raw( 'fetch', '--depth=1', 'origin', options.testsBranch )
- .raw( 'checkout', options.testsBranch );
- }
+ logAtIndent( 2, 'Copying source to:', formats.success( testRunnerDir ) );
+ await runShellScript( `cp -R ${ sourceDir } ${ testRunnerDir }` );
- log( ' >> Installing dependencies and building packages' );
+ logAtIndent(
+ 2,
+ 'Checking out branch:',
+ formats.success( testRunnerBranch )
+ );
+ // @ts-ignore
+ await SimpleGit( testRunnerDir ).raw( 'checkout', testRunnerBranch );
+
+ logAtIndent( 2, 'Installing dependencies and building' );
await runShellScript(
- `bash -c "${ [
- 'source $HOME/.nvm/nvm.sh',
- 'nvm install',
- 'npm ci',
- 'npx playwright install chromium --with-deps',
- 'npm run build:packages',
- ].join( ' && ' ) }"`,
- performanceTestDirectory
+ `bash -c "source $HOME/.nvm/nvm.sh && nvm install && npm ci && npx playwright install chromium --with-deps && npm run build:packages"`,
+ testRunnerDir
);
- log( ' >> Creating the environment folders' );
- await runShellScript( 'mkdir -p ' + rootDirectory + '/envs' );
- /*
- * 2- Preparing the environment directories per branch.
- */
+ logAtIndent( 1, 'Setting up test environments' );
+
+ const envsDir = path.join( baseDir, 'environments' );
+ logAtIndent( 2, 'Creating parent directory:', formats.success( envsDir ) );
+ fs.mkdirSync( envsDir );
+
+ let wpZipUrl = null;
+ if ( options.wpVersion ) {
+ // In order to match the topology of ZIP files at wp.org, remap .0
+ // patch versions to major versions:
+ //
+ // 5.7 -> 5.7 (unchanged)
+ // 5.7.0 -> 5.7 (changed)
+ // 5.7.2 -> 5.7.2 (unchanged)
+ const zipVersion = options.wpVersion.replace( /^(\d+\.\d+).0/, '$1' );
+ wpZipUrl = `https://wordpress.org/wordpress-${ zipVersion }.zip`;
+ }
- log( '\n>> Preparing an environment directory per branch' );
- const branchDirectories = {};
+ const branchDirs = {};
for ( const branch of branches ) {
- log( ` >> Branch: ${ branch }` );
- const sanitizedBranch = sanitizeBranchName( branch );
- const environmentDirectory = rootDirectory + '/envs/' + sanitizedBranch;
+ logAtIndent( 2, 'Branch:', formats.success( branch ) );
+ const sanitizedBranchName = sanitizeBranchName( branch );
+ const envDir = path.join( envsDir, sanitizedBranchName );
+
+ logAtIndent( 3, 'Creating directory:', formats.success( envDir ) );
+ fs.mkdirSync( envDir );
// @ts-ignore
- branchDirectories[ branch ] = environmentDirectory;
- const buildPath = `${ environmentDirectory }/plugin`;
- await runShellScript( 'mkdir ' + environmentDirectory );
- await runShellScript( `cp -R ${ baseDirectory } ${ buildPath }` );
+ branchDirs[ branch ] = envDir;
+ const buildDir = path.join( envDir, 'plugin' );
- const fancyBranch = formats.success( branch );
+ logAtIndent( 3, 'Copying source to:', formats.success( buildDir ) );
+ await runShellScript( `cp -R ${ sourceDir } ${ buildDir }` );
- if ( branch === options.testsBranch ) {
- log(
- ` >> Re-using the testing branch for ${ fancyBranch }`
- );
- await runShellScript(
- `cp -R ${ performanceTestDirectory } ${ buildPath }`
- );
- } else {
- log( ` >> Fetching the ${ fancyBranch } branch` );
- // @ts-ignore
- await SimpleGit( buildPath ).reset( 'hard' ).checkout( branch );
- }
+ logAtIndent( 3, 'Checking out:', formats.success( branch ) );
+ // @ts-ignore
+ await SimpleGit( buildDir ).raw( 'checkout', branch );
- log( ` >> Building the ${ fancyBranch } branch` );
+ logAtIndent( 3, 'Installing dependencies and building' );
await runShellScript(
- 'bash -c "source $HOME/.nvm/nvm.sh && nvm install && npm ci && npm run prebuild:packages && node ./bin/packages/build.js && npx wp-scripts build"',
- buildPath
+ `bash -c "source $HOME/.nvm/nvm.sh && nvm install && npm ci && npm run build"`,
+ buildDir
+ );
+
+ const wpEnvConfigPath = path.join( envDir, '.wp-env.json' );
+
+ logAtIndent(
+ 3,
+ 'Saving wp-env config to:',
+ formats.success( wpEnvConfigPath )
);
- // Create the config file for the current env.
fs.writeFileSync(
- path.join( environmentDirectory, '.wp-env.json' ),
+ wpEnvConfigPath,
JSON.stringify(
{
config: {
WP_DEBUG: false,
SCRIPT_DEBUG: false,
},
- core: 'WordPress/WordPress',
- plugins: [ path.join( environmentDirectory, 'plugin' ) ],
- themes: [
- path.join(
- performanceTestDirectory,
- 'test/emptytheme'
- ),
- ],
+ core: wpZipUrl || 'WordPress/WordPress',
+ plugins: [ buildDir ],
+ themes: [ path.join( testRunnerDir, 'test/emptytheme' ) ],
env: {
tests: {
mappings: {
'wp-content/mu-plugins': path.join(
- performanceTestDirectory,
+ testRunnerDir,
'packages/e2e-tests/mu-plugins'
),
'wp-content/plugins/gutenberg-test-plugins':
path.join(
- performanceTestDirectory,
+ testRunnerDir,
'packages/e2e-tests/plugins'
),
'wp-content/themes/gutenberg-test-themes':
path.join(
- performanceTestDirectory,
+ testRunnerDir,
'test/gutenberg-test-themes'
),
'wp-content/themes/gutenberg-test-themes/twentytwentyone':
@@ -251,99 +305,61 @@ async function runPerformanceTests( branches, options ) {
),
'utf8'
);
-
- if ( options.wpVersion ) {
- // In order to match the topology of ZIP files at wp.org, remap .0
- // patch versions to major versions:
- //
- // 5.7 -> 5.7 (unchanged)
- // 5.7.0 -> 5.7 (changed)
- // 5.7.2 -> 5.7.2 (unchanged)
- const zipVersion = options.wpVersion.replace(
- /^(\d+\.\d+).0/,
- '$1'
- );
- const zipUrl = `https://wordpress.org/wordpress-${ zipVersion }.zip`;
- log( ` Using WordPress version ${ zipVersion }` );
-
- // Patch the environment's .wp-env.json config to use the specified WP
- // version:
- //
- // {
- // "core": "https://wordpress.org/wordpress-$VERSION.zip",
- // ...
- // }
- const confPath = `${ environmentDirectory }/.wp-env.json`;
- const conf = { ...readJSONFile( confPath ), core: zipUrl };
- await fs.writeFileSync(
- confPath,
- JSON.stringify( conf, null, 2 ),
- 'utf8'
- );
- }
}
- // Printing the used folders.
- log(
- '\n>> Perf Tests Directory : ' +
- formats.success( performanceTestDirectory )
- );
- for ( const branch of branches ) {
- // @ts-ignore
- const envPath = formats.success( branchDirectories[ branch ] );
- log( `>> Environment Directory (${ branch }) : ${ envPath }` );
- }
-
- /*
- * 3- Running the tests.
- */
-
- log( '\n>> Running the tests' );
+ logAtIndent( 0, 'Looking for test files' );
const testSuites = getFilesFromDir(
- path.join( performanceTestDirectory, 'test/performance/specs' )
- ).map( ( file ) => path.basename( file, '.spec.js' ) );
+ path.join( testRunnerDir, 'test/performance/specs' )
+ ).map( ( file ) => {
+ logAtIndent( 1, 'Found:', formats.success( file ) );
+ return path.basename( file, '.spec.js' );
+ } );
+
+ logAtIndent( 0, 'Running tests' );
+
+ if ( wpZipUrl ) {
+ logAtIndent(
+ 1,
+ 'Using:',
+ formats.success( `WordPress v${ options.wpVersion }` )
+ );
+ } else {
+ logAtIndent( 1, 'Using:', formats.success( 'WordPress trunk' ) );
+ }
- const wpEnvPath = path.join(
- performanceTestDirectory,
- 'node_modules/.bin/wp-env'
- );
+ const wpEnvPath = path.join( testRunnerDir, 'node_modules/.bin/wp-env' );
for ( const testSuite of testSuites ) {
for ( let i = 1; i <= TEST_ROUNDS; i++ ) {
- const roundInfo = `round ${ i } of ${ TEST_ROUNDS }`;
- log( ` >> Suite: ${ testSuite } (${ roundInfo })` );
+ logAtIndent(
+ 1,
+ // prettier-ignore
+ `Suite: ${ formats.success( testSuite ) } (round ${ i } of ${ TEST_ROUNDS })`
+ );
+
for ( const branch of branches ) {
- const sanitizedBranch = sanitizeBranchName( branch );
- const runKey = `${ testSuite }_${ sanitizedBranch }_round-${ i }`;
+ logAtIndent( 2, 'Branch:', formats.success( branch ) );
+
+ const sanitizedBranchName = sanitizeBranchName( branch );
+ const runKey = `${ testSuite }_${ sanitizedBranchName }_round-${ i }`;
// @ts-ignore
- const environmentDirectory = branchDirectories[ branch ];
- log( ` >> Branch: ${ branch }` );
- log( ' >> Starting the environment.' );
- await runShellScript(
- `${ wpEnvPath } start`,
- environmentDirectory
- );
- log( ' >> Running the test.' );
- await runTestSuite(
- testSuite,
- performanceTestDirectory,
- runKey
- );
- log( ' >> Stopping the environment' );
- await runShellScript(
- `${ wpEnvPath } stop`,
- environmentDirectory
- );
+ const envDir = branchDirs[ branch ];
+
+ logAtIndent( 3, 'Starting environment' );
+ await runShellScript( `${ wpEnvPath } start`, envDir );
+
+ logAtIndent( 3, 'Running tests' );
+ await runTestSuite( testSuite, testRunnerDir, runKey );
+
+ logAtIndent( 3, 'Stopping environment' );
+ await runShellScript( `${ wpEnvPath } stop`, envDir );
}
}
}
- /*
- * 4- Formatting and saving the results.
- */
+ logAtIndent( 0, 'Calculating results' );
- // Load curated results from each round.
const resultFiles = getFilesFromDir( ARTIFACTS_PATH ).filter( ( file ) =>
file.endsWith( RESULTS_FILE_SUFFIX )
);
@@ -352,17 +368,21 @@ async function runPerformanceTests( branches, options ) {
// Calculate medians from all rounds.
for ( const testSuite of testSuites ) {
- results[ testSuite ] = {};
+ logAtIndent( 1, 'Test suite:', formats.success( testSuite ) );
+ results[ testSuite ] = {};
for ( const branch of branches ) {
- const sanitizedBranch = sanitizeBranchName( branch );
+ const sanitizedBranchName = sanitizeBranchName( branch );
const resultsRounds = resultFiles
.filter( ( file ) =>
file.includes(
- `${ testSuite }_${ sanitizedBranch }_round-`
+ `${ testSuite }_${ sanitizedBranchName }_round-`
)
)
- .map( ( file ) => readJSONFile( file ) );
+ .map( ( file ) => {
+ logAtIndent( 2, 'Reading from:', formats.success( file ) );
+ return readJSONFile( file );
+ } );
const metrics = Object.keys( resultsRounds[ 0 ] );
results[ testSuite ][ branch ] = {};
@@ -378,25 +398,31 @@ async function runPerformanceTests( branches, options ) {
}
}
}
+ const calculatedResultsPath = path.join(
+ ARTIFACTS_PATH,
+ testSuite + RESULTS_FILE_SUFFIX
+ );
- // Save calculated results to file.
+ logAtIndent(
+ 2,
+ 'Saving curated results to:',
+ formats.success( calculatedResultsPath )
+ );
fs.writeFileSync(
- path.join( ARTIFACTS_PATH, testSuite + RESULTS_FILE_SUFFIX ),
+ calculatedResultsPath,
JSON.stringify( results[ testSuite ], null, 2 )
);
}
- /*
- * 5- Displaying the results.
- */
-
- log( '\n>> š Results.\n' );
+ logAtIndent( 0, 'Printing results' );
log(
- '\nPlease note that client side metrics EXCLUDE the server response time.\n'
+ formats.warning(
+ '\nPlease note that client side metrics EXCLUDE the server response time.'
+ )
);
for ( const testSuite of testSuites ) {
- log( `\n>> ${ testSuite }\n` );
+ logAtIndent( 0, formats.success( testSuite ) );
// Invert the results so we can display them in a table.
/** @type {Record>} */
diff --git a/bin/plugin/commands/test/__snapshots__/changelog.js.snap b/bin/plugin/commands/test/__snapshots__/changelog.js.snap
index 571019ea3dca9..9ecb797fa5683 100644
--- a/bin/plugin/commands/test/__snapshots__/changelog.js.snap
+++ b/bin/plugin/commands/test/__snapshots__/changelog.js.snap
@@ -89,6 +89,7 @@ exports[`getChangelog verify that the changelog is properly formatted 1`] = `
### Performance
+- Add search performance measure and make other measures more stable. ([33848](https://github.com/WordPress/gutenberg/pull/33848))
- Avoid double parsing the content when loading the editor. ([33727](https://github.com/WordPress/gutenberg/pull/33727))
#### Block Library
@@ -170,7 +171,6 @@ exports[`getChangelog verify that the changelog is properly formatted 1`] = `
- Scripts: Update webpack to v5 (try 2). ([33818](https://github.com/WordPress/gutenberg/pull/33818))
#### Testing
-- Add search performance measure and make other measures more stable. ([33848](https://github.com/WordPress/gutenberg/pull/33848))
- E2E: Block Hierarchy Navigation wait for the column to be highlighted. ([33721](https://github.com/WordPress/gutenberg/pull/33721))
diff --git a/changelog.txt b/changelog.txt
index 0f4f5bcf9bbcf..6d47c98854458 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,6 +1,6 @@
== Changelog ==
-= 16.6.0-rc.1 =
+= 16.6.0 =
## Changelog
@@ -10,7 +10,7 @@
- Add Slot and Fill directives. ([53958](https://github.com/WordPress/gutenberg/pull/53958))
- Query block: Client-side pagination. ([53812](https://github.com/WordPress/gutenberg/pull/53812))
- Update `data-wp-bind` directive logic. ([54003](https://github.com/WordPress/gutenberg/pull/54003))
-
+- Query block: Move "Enhance pagination" toggle under Settings. ([54198](https://github.com/WordPress/gutenberg/pull/54198))
### Enhancements
@@ -164,6 +164,11 @@
#### Block API
- Auto-inserting blocks: Add block inspector panel. ([52969](https://github.com/WordPress/gutenberg/pull/52969))
+- - Auto-inserting blocks: Add block icon to block inspector panel toggles. ([54029](https://github.com/WordPress/gutenberg/pull/54029))
+- Auto-inserting blocks: Remove toggle if block is present elsewhere. ([54024](https://github.com/WordPress/gutenberg/pull/54024))
+- Rename "auto inserting blocks" to "block hooks". ([54147](https://github.com/WordPress/gutenberg/pull/54147))
+
+
### Documentation
@@ -196,6 +201,9 @@
- Enforce valid function names in the packages/block-library/src/*/*.php files. ([53438](https://github.com/WordPress/gutenberg/pull/53438))
- Fonts Library: Update properties name from snake case to camel case to match the rest of the properties. ([53746](https://github.com/WordPress/gutenberg/pull/53746))
+#### Block API
+- Auto-inserting blocks: Remove obsolete TODO, fix REST API field description. ([54208](https://github.com/WordPress/gutenberg/pull/54208))
+
#### Post Editor
- Editor: Fix the 'useSelect' warning in the 'useIsDirty' hook. ([53759](https://github.com/WordPress/gutenberg/pull/53759))
- Fix browser console error when changing device preview mode. ([53969](https://github.com/WordPress/gutenberg/pull/53969))
@@ -250,6 +258,8 @@ The following contributors merged PRs in this release:
@afercia @andrewserong @anton-vlasenko @bangank36 @brookewp @ciampo @colorful-tones @DAreRodz @dcalhoun @derekblank @ellatrix @felixarntz @geriux @glendaviesnz @gziolo @hellofromtonya @jasmussen @jblz @JEverhart383 @jordesign @jorgefilipecosta @jsnajdr @juanmaguitar @krokodok @luisherranz @Mamaduka @margolisj @matiasbenedetto @mburridge @mirka @mklute101 @mokagio @ndiego @ntsekouras @oandregal @ocean90 @ockham @priethor @ramonjd @richtabor @SiobhyB @Smit2808 @stokesman @t-hamano @torounit @tyxla @walbo @WunderBart @youknowriad
+
+
= 16.5.1 =
diff --git a/composer.json b/composer.json
index 134e366befdb9..3571377bd58bd 100644
--- a/composer.json
+++ b/composer.json
@@ -26,10 +26,8 @@
}
},
"require-dev": {
- "dealerdirect/phpcodesniffer-composer-installer": "^0.7",
- "squizlabs/php_codesniffer": "^3.5",
"phpcompatibility/phpcompatibility-wp": "^2.1.3",
- "wp-coding-standards/wpcs": "^2.2",
+ "wp-coding-standards/wpcs": "^3.0",
"sirbrillig/phpcs-variable-analysis": "^2.8",
"spatie/phpunit-watcher": "^1.23",
"yoast/phpunit-polyfills": "^1.0",
diff --git a/docs/README.md b/docs/README.md
index 41c694faf71f8..d04df59e95752 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -18,7 +18,7 @@ Through the Block editor, you create content modularly using Blocks. There are a
A [Block](https://developer.wordpress.org/block-editor/explanations/architecture/key-concepts/#blocks) is a discrete element such as a Paragraph, Heading, Media element, or Embed. Each block is treated as a separate element with individual editing and format controls. When all these components are pieced together, they make up the content that is then [stored in the WordPress database](https://developer.wordpress.org/block-editor/explanations/architecture/data-flow/#serialization-and-parsing).
-The Block Editor is the result of the [work done on the **Gutenberg project**](https://developer.wordpress.org/block-editor/explanations/faq/#what-is-gutenberg) which is aimed to revolutionize the WordPress editing experience.
+The Block Editor is the result of the [work done on the **Gutenberg project**](https://developer.wordpress.org/block-editor/getting-started/faq/#what-is-gutenberg) which is aimed to revolutionize the WordPress editing experience.
Besides offering an [enhanced editing experience](https://wordpress.org/gutenberg/) through visual content creation tools, the Block Editor is also a powerful developer platform with a [rich feature set of APIs](https://developer.wordpress.org/block-editor/reference-guides/) that allow it to be manipulated and extended in a multitude of different ways.
@@ -29,7 +29,7 @@ This handbook is focused on block development and is divided into five sections,
**[Getting Started](https://developer.wordpress.org/block-editor/getting-started/)**
-For those just starting out with block development this is where you can get set up with a [development environment](https://developer.wordpress.org/block-editor/getting-started/devenv/) and learn the [fundamentals of block development](https://developer.wordpress.org/block-editor/getting-started/create-block/).
+For those just starting out with block development this is where you can get set up with a [development environment](https://developer.wordpress.org/block-editor/getting-started/devenv/) and learn the [fundamentals of block development](https://developer.wordpress.org/block-editor/getting-started/create-block/). Its [Glossary of terms](https://developer.wordpress.org/block-editor/getting-started/glossary/) and [FAQs](https://developer.wordpress.org/block-editor/getting-started/faq/) should answer any outstanding questions you may have.
**[How-to Guides](https://developer.wordpress.org/block-editor/how-to-guides/)**
@@ -44,8 +44,7 @@ This section is the heart of the handbook and is where you can get down to the n
**[Explanations](https://developer.wordpress.org/block-editor/explanations/)**
-This section enables you to go deeper and reinforce your practical knowledge with a theoretical understanding of the [Architecture](https://developer.wordpress.org/block-editor/explanations/architecture/) of the block editor. Its [Glossary of terms](https://developer.wordpress.org/block-editor/explanations/glossary/) and [FAQs](https://developer.wordpress.org/block-editor/explanations/faq/) should answer any outstanding questions you may have.
-
+This section enables you to go deeper and reinforce your practical knowledge with a theoretical understanding of the [Architecture](https://developer.wordpress.org/block-editor/explanations/architecture/) of the block editor.
**[Contributor Guide](https://developer.wordpress.org/block-editor/contributors/)**
diff --git a/docs/final-g-wapuu-black.svg b/docs/contributors/assets/gutenberg-logo-black.svg
similarity index 100%
rename from docs/final-g-wapuu-black.svg
rename to docs/contributors/assets/gutenberg-logo-black.svg
diff --git a/docs/contributors/code/coding-guidelines.md b/docs/contributors/code/coding-guidelines.md
index e7ff0a4084306..12c3ad96cb85f 100644
--- a/docs/contributors/code/coding-guidelines.md
+++ b/docs/contributors/code/coding-guidelines.md
@@ -207,24 +207,25 @@ You can attach private selectors and actions to a public store:
```js
// In packages/package1/store.js:
-import { privateHasContentRoleAttribute, ...selectors } from './selectors';
-import { privateToggleFeature, ...actions } from './selectors';
+import { privateHasContentRoleAttribute } from './private-selectors';
+import { privateToggleFeature } from './private-actions';
// The `lock` function is exported from the internal private-apis.js file where
// the opt-in function was called.
import { lock, unlock } from './lock-unlock';
-export const store = registerStore(/* ... */);
+export const store = registerStore( /* ... */ );
// Attach a private action to the exported store:
-unlock( store ).registerPrivateActions({
- privateToggleFeature
+unlock( store ).registerPrivateActions( {
+ privateToggleFeature,
} );
// Attach a private action to the exported store:
-unlock( store ).registerPrivateSelectors({
- privateHasContentRoleAttribute
+unlock( store ).registerPrivateSelectors( {
+ privateHasContentRoleAttribute,
} );
+```
-
+```js
// In packages/package2/MyComponent.js:
import { store } from '@wordpress/package1';
import { useSelect } from '@wordpress/data';
@@ -233,17 +234,18 @@ import { useSelect } from '@wordpress/data';
import { unlock } from './lock-unlock';
function MyComponent() {
- const hasRole = useSelect( ( select ) => (
- // Use the private selector:
- unlock( select( store ) ).privateHasContentRoleAttribute()
+ const hasRole = useSelect(
+ ( select ) =>
+ // Use the private selector:
+ unlock( select( store ) ).privateHasContentRoleAttribute()
// Note the unlock() is required. This line wouldn't work:
- // select( store ).privateHasContentRoleAttribute()
- ) );
+ // select( store ).privateHasContentRoleAttribute()
+ );
// Use the private action:
unlock( useDispatch( store ) ).privateToggleFeature();
- // ...
+ // ...
}
```
@@ -263,7 +265,9 @@ lock( privateApis, {
privateClass: class PrivateClass {},
privateVariable: 5,
} );
+```
+```js
// In packages/package2/index.js:
import { privateApis } from '@wordpress/package1';
import { unlock } from './lock-unlock';
@@ -336,7 +340,9 @@ export function validateBlocks( blocks ) {
export const privateApis = {};
lock( privateApis, { privateValidateBlocks } );
+```
+```js
// In @wordpress/package2/index.js:
import { privateApis as package1PrivateApis } from '@wordpress/package1';
import { unlock } from './lock-unlock';
@@ -363,30 +369,30 @@ const PrivateMyButton = ( { title, privateShowIcon = true } ) => {
return (
);
-}
+};
// The stable public component is a thin wrapper that calls the
// private component with the private features disabled
-export const MyButton = ( { title } ) =>
-
+export const MyButton = ( { title } ) => (
+
+);
export const privateApis = {};
lock( privateApis, { PrivateMyButton } );
+```
-
+```js
// In @wordpress/package2/index.js:
import { privateApis } from '@wordpress/package1';
import { unlock } from './lock-unlock';
// The private component may be "unlocked" given the stable component:
-const { PrivateMyButton } = unlock(privateApis);
+const { PrivateMyButton } = unlock( privateApis );
export function MyComponent() {
- return (
-
- )
+ return ;
}
```
@@ -438,13 +444,14 @@ function privateInCorePublicInPlugin() {}
// Gutenberg treats both functions as private APIs internally:
const privateApis = {};
-lock(privateApis, { privateEverywhere, privateInCorePublicInPlugin });
+lock( privateApis, { privateEverywhere, privateInCorePublicInPlugin } );
// The privateInCorePublicInPlugin function is explicitly exported,
// but this export will not be merged into WordPress core thanks to
// the process.env.IS_GUTENBERG_PLUGIN check.
if ( process.env.IS_GUTENBERG_PLUGIN ) {
- export const privateInCorePublicInPlugin = unlock( privateApis ).privateInCorePublicInPlugin;
+ export const privateInCorePublicInPlugin =
+ unlock( privateApis ).privateInCorePublicInPlugin;
}
```
@@ -459,7 +466,7 @@ const a = 10;
// Bad:
const object = {
a: a,
- performAction: function() {
+ performAction: function () {
// ...
},
};
@@ -771,7 +778,7 @@ Documenting a function component should be treated the same as any other functio
*
* @return {?string} Block title.
*/
-```
+````
For class components, there is no recommendation for documenting the props of the component. Gutenberg does not use or endorse the [`propTypes` static class member](https://react.dev/reference/react/Component#static-proptypes).
diff --git a/docs/contributors/design/README.md b/docs/contributors/design/README.md
index acf1b6cc9b234..de390937244ed 100644
--- a/docs/contributors/design/README.md
+++ b/docs/contributors/design/README.md
@@ -20,7 +20,9 @@ The [WordPress Design team](http://make.wordpress.org/design/) uses [Figma](http
This section outlines the design principles and patterns of the editor interfaceāto explain the background of the design, inform future improvements, and help people design great blocks.
-![Gutenberg Logo](https://cldup.com/J2MgjuShPv-3000x3000.png)
+
+
+The Gutenberg logo was made by [Cristel Rossignol](https://twitter.com/cristelrossi), and is released under the GPL license. [Download the SVG logo](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/contributors/assets/gutenberg-logo-black.svg).
### Goal of Gutenberg
@@ -57,26 +59,3 @@ Gutenberg wants to make it easier to author rich content. This means ensuring go
**Direct manipulation is intuitive.** The block interface allows users to manipulate content directly on the page. Plugin and theme authors will support and extend this experience by building their own custom blocks.
**Code editing shouldn't be necessary for customization.** Customizing traditionally required complicated markup, and complicated markup is easy to break. With Gutenberg, customizing becomes more intuitive ā and safer. A developer will be able to provide custom blocks that directly render portions of a layout (a three column grid of features, for instance) and clearly specify what can be directly edited by the user. That means the user can update text, swap images, reduce the number of columns, without having to ask a developer, or worrying about breaking things.
-
-### Future Opportunities
-
-The initial phase of Gutenberg as described in the kickoff goal is primarily limited to the content area (specifically `post_content`) of posts and pages. Within those confines, we are embracing the web as a vertical river of content by appending blocks sequentially, then adding layout options to each block.
-
-That said, there isnāt any fixed limit to the kind of layouts Gutenberg will be able to create. Itās very possible for Gutenberg to grow beyond the confines of post and page content, to include the whole page ā one could think of a theme template as a comma-separated list of blocks, like this:
-
-```js
-{
- 'theme/header',
- 'theme/sidebar',
- 'core/content' {
- 'core/cover-image',
- 'theme/author-card',
- 'core/text',
- },
- 'theme/footer',
-}
-```
-
-Every block nested inside the content block would be _rearrangeable_. Every block would be _editable_. Every block would use the same API, and both the editor and the theme would load the same `style.css` file directly. In the end, both the editor/page builder and theme/front-end would appear near-identical, allowing for a true WYSIWYG experience.
-
-This concept is speculative, but itās one direction Gutenberg could go in the future.
diff --git a/docs/contributors/design/reference.md b/docs/contributors/design/reference.md
deleted file mode 100644
index 13cec883926a3..0000000000000
--- a/docs/contributors/design/reference.md
+++ /dev/null
@@ -1,18 +0,0 @@
-# Reference
-
-- [Glossary](/docs/explanations/glossary.md)
-- [Coding Guidelines](/docs/contributors/code/coding-guidelines.md)
-- [Testing Overview](/docs/contributors/code/testing-overview.md)
-- [Frequently Asked Questions](/docs/explanations/faq.md)
-
-## Logo
-
-
-
-Released under GPL license, made by [Cristel Rossignol](https://twitter.com/cristelrossi).
-
-[Download the SVG logo](https://github.com/WordPress/gutenberg/blob/HEAD/docs/final-g-wapuu-black.svg).
-
-## Mockups
-
-Mockup Sketch files are available in [the User Interface section](/docs/explanations/user-interface/design-resources.md).
diff --git a/docs/contributors/triage.md b/docs/contributors/triage.md
index c44dc0a4b8843..477c7d7dc8170 100644
--- a/docs/contributors/triage.md
+++ b/docs/contributors/triage.md
@@ -1,8 +1,10 @@
-### Get involved in triage
+# Triage
-To keep the repository healthy, it needs to be triaged regularly. Triage is the practice of reviewing existing issues and pull requests to make sure theyāre relevant, actionable, and have all the information they need. Anyone can help triage, although youāll need to be a member of the triage team for the Gutenberg repository to modify an issueās labels or edit its title.
+To keep the repository healthy, it needs to be triaged regularly. **Triage is the practice of reviewing existing issues and pull requests to make sure theyāre relevant, actionable, and have all the information they need**. Anyone can help triage, although youāll need to be a member of the triage team for the Gutenberg repository to modify an issueās labels or edit its title.
-### Join the triage team
+> Besides this page, the [How to do triage on GitHub](https://learn.wordpress.org/tutorial/how-to-do-triage-on-github/) tutorial is another great resource to get introduced to triage
+
+## Join the triage team
The triage team is an open group of people with a particular role of making sure triage is done consistently across the Gutenberg repo. There are various types of triage which happen:
@@ -18,33 +20,33 @@ These are the expectations of being a triage team member:
If you would like to join this team, simply ask in #core-editor [Slack](https://make.wordpress.org/chat/) at any time.
-### Getting started
+## Triage your first issues
To start simply choose from one of these filtered lists below. Note: You can find most of these filters by selecting the āSortā option from the [overall Issues page](https://github.com/wordpress/gutenberg/issues).
-- All Gutenberg issues [without an assigned label](https://github.com/WordPress/gutenberg/issues?q=is%3Aopen+is%3Aissue+no%3Alabel+sort%3Aupdated-asc). Triaging by simply adding labels helps people focused on certain aspects of Gutenberg find relevant issues easier and start working on them.
-- All Gutenberg pull requests [without an assigned label](https://github.com/WordPress/gutenberg/pulls?q=is%3Aopen+is%3Apr+no%3Alabel). This requires a level of comfortability with code. For more guidance on which labels are best to use, please [review this section on labeling pull requests](/docs/contributors/repository-management.md#pull-requests) for contributors. You can also always check with the person authoring the pull request to make sure the labels match what they are intending to do.
-- [The least recently updated](https://github.com/WordPress/gutenberg/issues?q=is%3Aopen+is%3Aissue+sort%3Aupdated-asc) Gutenberg issues. Triaging issues that are getting old and possibly out of date keeps important work from being overlooked.
-- All Gutenberg issues [with no comments](https://github.com/wordpress/gutenberg/issues?q=is%3Aissue+is%3Aopen+comments%3A0+). Triaging this list helps make sure all issues are acknowledged, and can help identify issues that may need more information or discussion before they are actionable.
-- [The least commented](https://github.com/wordpress/gutenberg/issues?q=is%3Aissue+is%3Aopen+sort%3Acomments-asc) on Gutenberg issues. Triaging this list helps the community figure out what things might still need traction for certain proposals.
-- [The most commented](https://github.com/wordpress/gutenberg/issues?q=is%3Aissue+is%3Aopen+sort%3Acomments-desc) on Gutenberg issues. If you feel comfortable chiming in and the conversation has stagnated, the best way to triage these kinds of issues is to summarize the discussion thus far and do your best to identify action items, blockers, etc. Triaging this list allows finding solutions to important and complex issues to move forward.
-- You can also create your own custom set of filters on GitHub. If you have a filter you think might be useful for the community, feel free to submit a PR to add it to this list.
+- **All Gutenberg issues [without an assigned label](https://github.com/WordPress/gutenberg/issues?q=is%3Aopen+is%3Aissue+no%3Alabel+sort%3Aupdated-asc)**. Triaging by simply adding labels helps people focused on certain aspects of Gutenberg find relevant issues easier and start working on them.
+- **All Gutenberg pull requests [without an assigned label](https://github.com/WordPress/gutenberg/pulls?q=is%3Aopen+is%3Apr+no%3Alabel)**. This requires a level of comfortability with code. For more guidance on which labels are best to use, please [review this section on labeling pull requests](/docs/contributors/repository-management.md#pull-requests) for contributors. You can also always check with the person authoring the pull request to make sure the labels match what they are intending to do.
+- **[The least recently updated](https://github.com/WordPress/gutenberg/issues?q=is%3Aopen+is%3Aissue+sort%3Aupdated-asc) Gutenberg issues**. Triaging issues that are getting old and possibly out of date keeps important work from being overlooked.
+- **All Gutenberg issues [with no comments](https://github.com/wordpress/gutenberg/issues?q=is%3Aissue+is%3Aopen+comments%3A0+)**. Triaging this list helps make sure all issues are acknowledged, and can help identify issues that may need more information or discussion before they are actionable.
+- **[The least commented](https://github.com/wordpress/gutenberg/issues?q=is%3Aissue+is%3Aopen+sort%3Acomments-asc) on Gutenberg issues**. Triaging this list helps the community figure out what things might still need traction for certain proposals.
+- **[The most commented](https://github.com/wordpress/gutenberg/issues?q=is%3Aissue+is%3Aopen+sort%3Acomments-desc) on Gutenberg issues**. If you feel comfortable chiming in and the conversation has stagnated, the best way to triage these kinds of issues is to summarize the discussion thus far and do your best to identify action items, blockers, etc. Triaging this list allows finding solutions to important and complex issues to move forward.
+- You can also **create your own custom set of filters on GitHub**. If you have a filter you think might be useful for the community, feel free to submit a PR to add it to this list.
-### General triage process
+## General triage process
When triaging, either one of the lists above or issues in general, work through issues one-by-one. Here are some steps you can perform for each issue:
-- First search for duplicates. If the issue is duplicate, close it by commenting with āDuplicate of #ā and add any relevant new details to the existing issue. (Donāt forget to search for duplicates among closed issues as well!).
-- If the issue is missing labels, add some to better categorize it (requires proper permissions given after joining the triage team). A good starting place when adding labels is to apply one of the labels prefixed [Type] (e.g. [Type] Enhancement or [Type] Bug) to indicate what kind of issue it is. After that consider adding more descriptive labels. If the issue concerns a particular core block, add one of the labels prefixed [Block]. Or if the issue affects a particular feature there are [Feature] labels. Finally, there are labels that affect particular interest areas, like Accessibility and Internationalization. You can view all possible labels [here](https://github.com/WordPress/gutenberg/labels).
-- If the title doesnāt communicate the issue clearly enough, edit it for clarity (requires proper permissions). Specifically, weād recommend having the main feature the issue relates to in the beginning of the title ([example](https://github.com/WordPress/gutenberg/issues/6193)) and for the title to generally be as succinct yet descriptive as possible ([example](https://github.com/WordPress/gutenberg/issues/6193)).
-- If itās a bug report, test to confirm the report or add the Needs Testing label. If there is not enough information to confirm the report, add the [Status] Needs More Info label and ask for the details needed. Itās particularly beneficial when a bug report has steps for reproduction so ask the reporter to add those if theyāre missing.
-- Remove the [Status] Needs More Info if the author of the issue has responded with enough details.
-- Close the issue with a note if it has a [Status] Needs More Info label but the author didn't respond in 2+ weeks.
-- If there was a conversation on the issue but no actionable steps identified, follow up with the participants to see whatās actionable. Make sure to @ each participant when responding in a comment.
-- If you feel comfortable triaging the issue further, then you can also:
- - Check that the bug report is valid by debugging it to see if you can track down the technical specifics.
- - Check if the issue is missing some detail and see if you can fill in those details. For instance, if a bug report is missing visual detail, itās helpful to reproduce the issue locally and upload a screenshot or GIF.
- - Consider adding the Good First Issue label if you believe this is a relatively easy issue for a first-time contributor to try to solve.
+1. First **search for duplicates**. If the issue is duplicate, close it by commenting with āDuplicate of #ā and add any relevant new details to the existing issue. (Donāt forget to search for duplicates among closed issues as well!).
+2. If the **issue is missing labels, add some** to better categorize it (requires proper permissions given after joining the triage team). A good starting place when adding labels is to apply one of the labels prefixed [Type] (e.g. [Type] Enhancement or [Type] Bug) to indicate what kind of issue it is. After that consider adding more descriptive labels. If the issue concerns a particular core block, add one of the labels prefixed [Block]. Or if the issue affects a particular feature there are [Feature] labels. Finally, there are labels that affect particular interest areas, like Accessibility and Internationalization. You can view all possible labels [here](https://github.com/WordPress/gutenberg/labels).
+3. If the **title doesnāt communicate the issue clearly enough, edit it for clarity** (requires proper permissions). Specifically, weād recommend having the main feature the issue relates to in the beginning of the title ([example](https://github.com/WordPress/gutenberg/issues/6193)) and for the title to generally be as succinct yet descriptive as possible ([example](https://github.com/WordPress/gutenberg/issues/6193)).
+4. If itās a **bug report, test to confirm the report or add the `Needs Testing` label**. If there is not enough information to confirm the report, add the `[Status] Needs More Info` label and ask for the details needed. Itās particularly beneficial when a bug report has steps for reproduction so ask the reporter to add those if theyāre missing.
+5. **Remove the `[Status] Needs More Info` when is no longer needed**, for example if the author of the issue has responded with enough details.
+6. **Close the inactive `[Status] Needs More Info` issues with a note** if the author didn't respond in 2+ weeks.
+7. If there was a conversation on the issue but **no actionable steps identified, follow up with the participants to see whatās actionable**. Make sure to @ each participant when responding in a comment.
+8. If you feel comfortable triaging the issue further, then you can also:
+ - Check that the bug report is valid by debugging it to see if you can track down the technical specifics.
+ - Check if the issue is missing some detail and see if you can fill in those details. For instance, if a bug report is missing visual detail, itās helpful to reproduce the issue locally and upload a screenshot or GIF.
+ - Consider adding the Good First Issue label if you believe this is a relatively easy issue for a first-time contributor to try to solve.
**Commonly Used Labels**
@@ -65,11 +67,10 @@ If you have enough knowledge about the report at hand and feel confident in doin
| Label | Reason |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `Priority OMGWTFBBQ` | Major issues that are causing failures and are reported frequently. Typically, these are issues that are critical because they break important behavior or functionality. An example might be, āUnable to remove a block after it is added to the editorā. |
| `Priority: High` | Fits one of the current focuses and is causing a major broken experience (including flow, visual bugs and blocks). |
| `Priority: Low` | Enhancements that arenāt part of focuses, niche bugs, problems with old browsers. |
-### Closing issues
+## Closing issues
Issues are closed for the following reasons:
@@ -80,21 +81,23 @@ Issues are closed for the following reasons:
- An issue that needs more information that the author of the issue hasn't responded to for 2+ weeks.
- An item that is determined as unable to be fixed or is working as intended.
+## Specific Triages
+
### Release specific triage
Here are some guidelines to follow when doing triage specifically around the time of a release. This is important to differentiate compared to general triage so problematic, release blocking bugs are properly identified and solutions are found.
-- If a bug is introduced in a release candidate (RC) and it's going to break many workflows, add it to the version milestone and flag in the #core-editor channel in WordPress.org slack.
-- If a bug was introduced in the most recent version, and a next RC hasnāt yet happened, ideally the developers can push to fix it prior to RC! The amount of push for a fix should scale proportional to the potential of breakage. In this case, add to the RC milestone and, if deemed urgent, ping in the #core-editor channel in WordPress.org slack.
-- If a bug wasnāt introduced in the most recent version, do not add a milestone. Instead, use labels like ā[Priority] Highā if itās a pressing issue, and if needed you can call attention to it in the weekly core meetings.
+- **If a bug is introduced in a release candidate (RC) and it's going to break many workflows**, add it to the version milestone and flag in the [#core-editor](https://wordpress.slack.com/archives/C02QB2JS7) channel in WordPress.org slack.
+- **If a bug was introduced in the most recent version, and a next RC hasnāt yet happened**, ideally the developers can push to fix it prior to RC! The amount of push for a fix should scale proportional to the potential of breakage. In this case, add to the RC milestone and, if deemed urgent, ping in the [#core-editor](https://wordpress.slack.com/archives/C02QB2JS7) channel in WordPress.org slack.
+- **If a bug wasnāt introduced in the most recent version**, do not add a milestone. Instead, use labels like `[Priority] High` if itās a pressing issue, and if needed you can call attention to it in the weekly core meetings.
### Design specific triage
Along with the general triage flows listed previously, there are some specific additions to the flows for more design-centric triage for design minded folks participating in triage.
- PR testing and reviews: this should be your first stop for daily self triage.
-- Needs design feedback: check if the issue does need design feedback and, if possible, give it. You can organize this by priority, project boards or by least commented. Once there are enough opinions, please remove this label and decide on next steps (ie adding the Needs Design label).
-- Needs design: Does it really need a design? Does this fit a focus? If it has a design mark as āneeds design feedbackā to better categorize the issue.
+- Label `Needs Design Feedback`: check if the issue does need design feedback and, if possible, give it. You can organize this by priority, project boards or by least commented. Once there are enough opinions, please remove this label and decide on next steps (ie adding the Needs Design label).
+- Label `Needs Design`: Does it really need a design? Does this fit a focus? If it has a design mark as `Needs Design Feedback` to better categorize the issue.
Reminders:
diff --git a/docs/contributors/versions-in-wordpress.md b/docs/contributors/versions-in-wordpress.md
index 6709d124b242b..c4e25c3b2095e 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 |
| ------------------ | ----------------- |
+| 15.2-16.1 | 6.3.1 |
| 15.2-16.1 | 6.3 |
| 14.2-15.1 | 6.2 |
| 13.1-14.1 | 6.1.1 |
diff --git a/docs/explanations/architecture/full-site-editing-templates.md b/docs/explanations/architecture/full-site-editing-templates.md
index aa18ad17a18f6..a1138b4cdad18 100644
--- a/docs/explanations/architecture/full-site-editing-templates.md
+++ b/docs/explanations/architecture/full-site-editing-templates.md
@@ -2,7 +2,7 @@
## Template and template part flows
-This document will explain the internals of how templates and templates parts are rendered in the frontend and edited in the backend. For an introduction about block themes and Full site editing templates, refer to the [block theme documentation](/docs/how-to-guides/themes/block-theme-overview.md).
+This document will explain the internals of how templates and templates parts are rendered in the frontend and edited in the backend.
## Storage
diff --git a/docs/explanations/history.md b/docs/explanations/history.md
index db2b7518bd896..c5d8cbf8554f5 100644
--- a/docs/explanations/history.md
+++ b/docs/explanations/history.md
@@ -2,22 +2,35 @@
A set of links and resources covering the history of the Gutenberg project, how it got started, sources of inspiration, and initial thinking.
-## Survey
-
-There was a survey done: [https://make.wordpress.org/core/2017/04/07/editor-experience-survey-results/](https://make.wordpress.org/core/2017/04/07/editor-experience-survey-results/)
-
## Inspiration
-This includes a list of historical articles and influences on the Gutenberg project.
+A community [Editor Experience Survey](https://make.wordpress.org/core/2017/04/07/editor-experience-survey-results/) was conducted in early 2017, which reinforced the need for a new editing experience in WordPress.
-- LivingDocs: [https://beta.livingdocs.io/articles](https://beta.livingdocs.io/articles)
-- Parrot: [https://intenseminimalism.com/2017/parrot-an-integrated-site-builder-and-editor-concept-for-wordpress/](https://intenseminimalism.com/2017/parrot-an-integrated-site-builder-and-editor-concept-for-wordpress/)
+This is a list of historical articles and products that influenced the Gutenberg project and the creation of the Block Editor.
+
+- [Parrot: an integrated site builder and editor concept for WordPress](https://intenseminimalism.com/2017/parrot-an-integrated-site-builder-and-editor-concept-for-wordpress/)
+- LivingDocs
- Apple Keynote
- Slack
- Google Sites v2
-## Blog posts by the team
-
-- Gutenberg tag on make/core: updates and much more: [https://make.wordpress.org/core/tag/gutenberg/](https://make.wordpress.org/core/tag/gutenberg/)
-- Suggested revised timeline: [https://make.wordpress.org/core/2017/08/11/revised-suggested-roadmap-for-gutenberg-and-customization/](https://make.wordpress.org/core/2017/08/11/revised-suggested-roadmap-for-gutenberg-and-customization/)
-- Discovering Gutenberg: [https://make.wordpress.org/core/2017/08/08/discovering-gutenberg-and-next-steps/](https://make.wordpress.org/core/2017/08/08/discovering-gutenberg-and-next-steps/)
+### Gutenberg updates and feature overviews
+
+- [Themes of the Future](https://wordpress.tv/2021/01/21/eileen-violini-themes-of-the-future-the-new-frontier-of-gutenberg-block-based-themes-and-theme-development/), Eileen Violini (January 2021)
+- [Status Check: Site Editing & Customization](https://make.wordpress.org/core/2020/12/10/status-check-site-editing-and-customization/), MatĆas Ventura Bausero (December 2020)
+- [State of the Word 2020 FSE Demo](https://youtu.be/QI3qCoiuG3w?t=1279), Matt Mullenweg (December 2020)
+- [Embrace the Modularity](https://riad.blog/2020/01/28/embrace-the-modularity/), Riad Benguella (January 2020)
+- [State of the Word 2019 Gutenberg Demo](https://www.youtube.com/watch?v=LezbkeV059Q), Matt Mullenweg (December 2019)
+- [Growing JavaScript Skills with WordPress](https://gziolo.pl/2019/07/15/growing-javascript-skills-with-wordpress/) at JavaScript for WordPress conference (July 2019)
+- [Beyond Gutenberg](https://wordpress.tv/2018/07/09/matias-ventura-beyond-gutenberg/), MatĆas Ventura Bausero (July 2018)
+- [Anatomy of a block: Gutenberg design patterns](https://wordpress.tv/2018/07/08/tammie-lister-anatomy-of-a-block-gutenberg-design-patterns/), Tammie Lister (July 2018)
+- [The Language of Gutenberg](https://lamda.blog/2018/04/22/the-language-of-gutenberg/), Miguel Fonseca (April 2018)
+- [State of the Word 2017 Gutenberg Demo](https://youtu.be/XOY3ZUO6P0k?t=2100), Matt Mullenweg with demo by MatĆas Ventura Bausero (December 2017)
+- [Gutenberg, or the Ship of Theseus](https://matiasventura.com/post/gutenberg-or-the-ship-of-theseus/), MatĆas Ventura Bausero (October 2017)
+- [We Called It Gutenberg for a Reason](https://ma.tt/2017/08/we-called-it-gutenberg-for-a-reason/), Matt Mullenweg (August 2017)
+- [How Gutenberg is Changing WordPress Development](https://riad.blog/2017/10/06/how-gutenberg-is-changing-wordpress-development/), Riad Benguella (October 2017)
+- [Revised suggested roadmap for Gutenberg and Customization](https://make.wordpress.org/core/2017/08/11/revised-suggested-roadmap-for-gutenberg-and-customization/), Tammie Lister (August 2017)
+- [Discovering Gutenberg and next steps](https://make.wordpress.org/core/2017/08/08/discovering-gutenberg-and-next-steps/), Tammie Lister (August 2017)
+- [How Gutenberg Will Shape the Future of WordPress](https://www.linkedin.com/pulse/gutenberg-morten-rand-hendriksen/), Morten Rand-Henrikson (August 2017)
+
+You can also view this [Index of Gutenberg related posts](https://make.wordpress.org/core/handbook/references/keeping-up-with-gutenberg-index/) for more resources.
\ No newline at end of file
diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md
index ae1ba6557db8d..136caa5607e48 100644
--- a/docs/getting-started/README.md
+++ b/docs/getting-started/README.md
@@ -15,12 +15,16 @@ Welcome! Let's get started building with blocks. Blocks are at the core of exten
[Create a Block Tutorial](/docs/getting-started/create-block/README.md) - Learn how to create your first block for the WordPress block editor.
-[Full Site Editing](/docs/getting-started/full-site-editing.md) - Full Site Editing (FSE) is an umbrella project name for the collection of features that bring the experience and extendability of blocks to all parts of your siteāfrom settings and styles, to templates and themes, and more.
+## Learn WordPress Courses
-- Learn [about using theme.json](/docs/how-to-guides/themes/theme-json.md) to define settings and styles for your theme.
+At [Learn WordPress](https://learn.wordpress.org/), you can find [tutorials](https://learn.wordpress.org/tutorials/), [courses](https://learn.wordpress.org/courses/), and [online workshops](https://learn.wordpress.org/online-workshops/) to learn more about developing for the Block Editor. Here is a selection of current offerings.
-- [Create a Block Theme](/docs/how-to-guides/themes/create-block-theme.md) - Learn how block themes use blocks to build templates and the theme.json to provide styles.
-- [Convert a Classic Theme to a Block Theme](https://developer.wordpress.org/themes/block-themes/converting-a-classic-theme-to-a-block-theme/) - Learn how to adopt various pieces of full site editing in your classic theme.
+- [Intro to Block Development: Build Your First Custom Block](https://learn.wordpress.org/course/introduction-to-block-development-build-your-first-custom-block/)
+- [Converting a Shortcode to a Block](https://learn.wordpress.org/course/converting-a-shortcode-to-a-block/)
+- [Using the WordPress Data Layer](https://learn.wordpress.org/course/using-the-wordpress-data-layer/)
+- [Registering Block Patterns](https://learn.wordpress.org/workshop/registering-block-patterns/)
+- [Intro to Gutenberg Block Development](https://learn.wordpress.org/workshop/intro-to-gutenberg-block-development/)
+- [Intro to Publishing with the Block Editor](https://learn.wordpress.org/workshop/intro-to-publishing-with-the-block-editor/)
## Ways to Stay Informed
@@ -54,7 +58,7 @@ The [WordPress.org Roadmap](https://wordpress.org/about/roadmap/) with Four Phas
Checking in on [issues](https://github.com/WordPress/gutenberg/issues) and [PRs](https://github.com/WordPress/gutenberg/pulls) on GitHub. This will give you a nearly real-time understanding of whatās being worked on by the developers and designers.
-- [Glossary](/docs/explanations/glossary.md)
-- [Frequently Asked Questions](/docs/explanations/faq.md)
+- [Glossary](/docs/getting-started/glossary.md)
+- [Frequently Asked Questions](/docs/getting-started/faq.md)
- [Project History](/docs/explanations/history.md)
-- [Outreach](/docs/getting-started/outreach.md)
+- [Gutenberg related Make posts](https://make.wordpress.org/core/handbook/references/keeping-up-with-gutenberg-index/)
\ No newline at end of file
diff --git a/docs/getting-started/devenv/README.md b/docs/getting-started/devenv/README.md
index 7c55bcb6393f1..d6e8d9e1ae24b 100644
--- a/docs/getting-started/devenv/README.md
+++ b/docs/getting-started/devenv/README.md
@@ -1,228 +1,51 @@
-# Development Environment
+# Block Development Environment
-This guide is for setting up your local environment for JavaScript development for creating plugins and tools to extend WordPress and the block editor. If you are looking to contribute to Gutenberg project itself, see additional documentation in the [Getting Started guide](/docs/contributors/code/getting-started-with-code-contribution.md).
+This guide will help you set up the right development environment to create blocks and other plugins that extend and modify the Block Editor in WordPress.
-A development environment is a catch-all term for what you need setup on your computer to work. The three main pieces needed for our development environment are:
+To contribute to the Gutenberg project itself, refer to the additional documentation in the [code contribution guide](/docs/contributors/code/getting-started-with-code-contribution.md).`
-1. Node/NPM Development Tools
-2. WordPress Development Site
-3. Code Editor
+A block development environment includes the tools you need on your computer to successfully develop for the Block Editor. The three essential requirements are:
-## Quick Start
+1. [Code editor](#code-editor)
+2. [Node.js development tools](#node-js-development-tools)
+3. [Local WordPress environment (site)](#local-wordpress-environment)
-Here is a summary of the guide. See each section for additional details and explanations.
+## Code editor
-**1. Install Node development tools**
+A code editor is used to write code, and you can use whichever editor you're most comfortable with. The key is having a way to open, edit, and save text files.
-Download and install [Node Version Manager](https://github.com/nvm-sh/nvm) (nvm)
+If you do not already have a preferred code editor, [Visual Studio Code](https://code.visualstudio.com/) (VS Code) is a popular choice for JavaScript development among Core contributors. It works well across the three major platforms (Windows, Linux, and Mac) and is open-source and actively maintained by Microsoft. VS Code also has a vibrant community providing plugins and extensions, including many for WordPress development.
-```
-curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
-```
+## Node.js development tools
-Quit and restart terminal
-Install Node.js v16.
+Node.js (`node`) is an open-source runtime environment that allows you to execute JavaScript outside of the web browser. While Node.js is not required for all WordPress JavaScript development, it's essential when working with modern JavaScript tools and developing for the Block Editor.
-```
-nvm install 16
-```
+Node.js and its accompanying development tools allow you to:
-**2. WordPress Development Site**
+- Install and run WordPress packages needed for Block Editor development, such as `wp-scripts`
+- Setup local WordPress environments with `wp-env` and `wp-now`
+- Use the latest ECMAScript features and write code in ESNext
+- Lint, format, and test JavaScript code
+- Scaffold custom blocks with the `create-block` package
-First download, install, and start [Docker Desktop](https://www.docker.com/products/docker-desktop) following the instructions for your OS.
+The list goes on. While modern JavaScript development can be challenging, WordPress provides several tools, like [`wp-scripts`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/) and [`create-block`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/), that streamline the process and are made possible by Node.js development tools.
-- Install WordPress environment tool
+**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 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.
-```
-npm -g install @wordpress/env
-```
+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.
-Start the environment from an existing plugin or theme directory, or a new working directory:
+- [Install Node.js for Mac and Linux](/docs/getting-started/devenv/nodejs-development-environment.md#node-js-installation-on-mac-and-linux-with-nvm)
+- [Install Node.js for Windows](/docs/getting-started/devenv/nodejs-development-environment.md#node-js-installation-on-windows-and-others)
-```
-wp-env start
-```
+## Local WordPress environment
-You will have a full WordPress site installed, navigate to: http://localhost:8888/ using your browser, log in to the WordPress dashboard at http://localhost:8888/wp-admin/ using Username "admin" and Password "password", without the quotes.
+A local WordPress environment (site) provides a controlled, efficient, and secure space for development, allowing you to build and test your code before deploying it to a production site. The [same requirements](https://en-gb.wordpress.org/about/requirements/) for WordPress apply to local sites.
-**3. Code Editor**
+Many tools are available for setting up a local WordPress environment on your computer. The Block Editor Handbook covers `wp-env` and `wp-now`, both of which are open-source and maintained by the WordPress project itself.
-You can use any text editor to write code. For example, [Visual Studio Code](https://code.visualstudio.com/) is a popular open-source editor. You can follow instructions on their site to install it for your OS.
+Refer to the individual guides below for setup instructions.
-## Node Development Tools
+- [Get started with `wp-env`](/docs/getting-started/devenv/get-started-with-wp-env.md)
+- [Get started with `wp-now`](/docs/getting-started/devenv/get-started-with-wp-now.md)
-The tools needed for development are **Node** and **NPM**. **Nodejs** is a runtime environment that allows running JavaScript outside of the browser. NPM is the Node Package Manager, it is used for installing dependencies and running scripts. The script `npx` is also installed with Nodejsāthis script is used to run packages not yet installedāwe will use `npx` to bootstrap a block.
-
-The tools are used to convert the JavaScript we are going to write into a format that browsers can run. This is called transpiling or the build step.
-
-For Mac and Linux, it is recommended to use the [Node Version Manager](https://github.com/nvm-sh/nvm) (nvm). Using `nvm` to install node allows installing specific versions, plus installs locally in your home directory and avoids any global permission issues.
-
-For Windows, or alternative installs, you can [download a Nodejs installer](https://nodejs.org/en/download/) directly from the main Node.js website, v14 is recommended. Installers are available for Windows and Mac, and binaries available for Linux. See Node.js site for additional installation methods.
-
-Here are the quick instructions to install using nvm, see the [full installation instructions](https://github.com/nvm-sh/nvm#installing-and-updating) for additional details.
-
-Run the following on the command-line to install nvm:
-
-```sh
-curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
-```
-
-Note: On macOS, the required developer tools are not installed by default, if not already installed you may be prompted to download the install.
-
-
-
-After installing nvm, you need to use it to install Node.js, to install v16, run:
-
-```sh
-nvm install 16
-```
-
-If there is an error running the above command, for example a common error that occurs is:
-
-```sh
-$ nvm install 16
-zsh: command not found: nvm
-```
-
-First, try quitting and restarting your terminal to pick up the installed config.
-
-If restarting did not resolve the problem, you might need to create the default profile file.
-
-On macOS Catalina, the default shell is zsh, to create the profile file type `touch ~/.zshrc` on the command-line. It is fine to run if the file already exists. Note, `~/` is a shortcut to your home directory. For Ubuntu, including WSL, the default profile is bash, use `touch ~/.bashrc` to create.
-
-After creating the profile file, re-run the install command:
-
-```sh
-nvm install 16
-```
-
-The important part after installing is being able to use them in your terminal. Open a terminal command-line and type `node -v` and `npm -v` to confirm they are installed.
-
-```sh
-> node -v
-v16.20.1
-
-> npm -v
-8.19.4
-```
-
-Your versions may not match exactly, that is fine. The minimum version for Node.js is >= 12 and for npm >= 6.9, using v14 will be supported until upgrade is required.
-
-## WordPress Development Site
-
-There are several ways to run WordPress locally on your own computer, or you could even develop on a cloud hosted computer, though this may be slower.
-
-The WordPress [wp-env package](https://www.npmjs.com/package/@wordpress/env) lets you set up a local WordPress environment for building and testing plugins and themes, without any additional configuration.
-
-The `wp-env` tool uses Docker to create a virtual machine that runs the WordPress site. There are instructions available for installing Docker on [Windows 10 Pro](https://docs.docker.com/docker-for-windows/install/), [other versions of Windows 10](https://docs.docker.com/docker-for-windows/wsl/), [macOS](https://docs.docker.com/docker-for-mac/install/), and [Linux](https://docs.docker.com/v17.12/install/linux/docker-ce/ubuntu/#install-using-the-convenience-script). If using Ubuntu, see our additional notes for [help installing Docker on Ubuntu](/docs/getting-started/devenv/docker-ubuntu.md).
-
-After you have installed Docker, go ahead and install the `wp-env` tool. This command will install the tool globally, which means you can run it from any directory:
-
-```sh
-npm -g install @wordpress/env
-```
-
-To confirm it is installed and available, run:
-
-```sh
-wp-env --version
-> 1.6.0
-```
-
-The `wp-env` script is used to create a Docker WordPress environment. You can use this script to start an environment with your plugin activated by running it from the directory containing your plugin. For example if you are following the create block tutorial, this would be in the generated directory like so:
-
-```sh
-npx @wordpress/create-block starter-block
-cd starter-block
-wp-env start
-```
-
-You can access your environment in your browser at: [http://localhost:8888/](http://localhost:8888/), the default username is `admin` and default password is `password`. For more information controlling the Docker environment see the [@wordpress/env package readme](/packages/env/README.md).
-
-When using the script while developing a single plugin, `wp-env start` can mount and activate the plugin automatically when run from the directory containing the plugin. Note: This also works for themes when run from the directory in which you are developing the theme.
-
-If you run `wp-env start` from a directory that is not a plugin or theme, a generic WordPress environment will be created. The script will display the following warning, it is fine if this is your intention.
-
-```
-!! Warning: could not find a .wp-env.json configuration file and could not determine if 'DIR' is a WordPress installation, a plugin, or a theme.
-```
-
-You can use the `.wp-env.json` configuration file to create an environment that works with multiple plugins and/or themes. See the [@wordpress/env package for additional details](/packages/env/README.md#wp-envjson).
-
-#### Troubleshooting
-
-A common issue when running `wp-env` is `Error while running docker-compose command.`
-
-- Check that Docker Desktop is started and running.
-- Check Docker Desktop dashboard for logs, restart, or remove existing VMs.
-
-If you see the error: `Host is already in use by another container`
-
-- The container is already running, or another one is. You can stop an existing container running use `wp-env stop` from the directory you started it.
-- If you do not remember the directory you started wp-env in, you can stop all containers with `docker stop $(docker ps -q)`. Please note, this will stop all containers, use caution with this command.
-
-### Alternative to Docker
-
-Docker is just one method to run a local WordPress environment. Block development and extending WordPress is done using normal plugins, so any WordPress environment can be used. Here are some alternatives that you can consider which do not require installing Docker.
-
-- [Local](https://localwp.com/) - Local is a single application you download and install. You will need to know where the plugin directory is located after install. If you create a site called `mywp` typically the plugin directory is installed at `~\Local Sites\mywp\app\public\wp-content\plugins`. When working with gutenberg it is recommended to place your install in your own gutenberg folder. This avoids potential issue with the build processes.
-
-- [WampServer](http://www.wampserver.com/en/) or [MAMP](https://www.mamp.info/) environments, both are quite similar to Local, combining a web server, PHP, and database. However these tools are not WordPress specific, so if you are not already using them, Local might be an easier option.
-
-- Remote server - you can work on a remote server, most hosts provide a quick WordPress setup. However, this will require additional time thorughout development syncing to the server, or working directly on the remote server.
-
-The important part is having a WordPress site installed, and know where and how to update files in the plugins directory.
-
-## Code Editor
-
-[Visual Studio Code](https://code.visualstudio.com/) is a popular code editor for JavaScript development. It works quite well across the three major platforms (Windows, Linux, and Mac), it is open-source and actively maintained by Microsoft. Plus Visual Studio Code has a vibrant community providing plugins and extensions; it is becoming the defacto standard for web development.
-
-Alternative editors include [Sublime Text](https://www.sublimetext.com/) that is also available across platforms, though is a commercial product; or other free alternatives include [Vim](https://www.vim.org/), [Atom](https://atom.io/), and [Notepad++](https://notepad-plus-plus.org/) all support standard JavaScript style development.
-
-You can use any editor you're comfortable with, it is more a personal preference. The development setup for WordPress block editor is a common JavaScript environment and most editors have plugins and support. The key is having a way to open, edit, and save text files.
-
-## Uninstall - Start Over
-
-Here are a few instructions if you need to start over, or want to remove what was installed.
-
-### Local Environment
-
-- If you just want to reset and clean the WordPress database:
-
-```
-wp-env clean all
-```
-
-- To remove the local environment completely for a specific project:
-
-```
-wp-env destroy
-```
-
-- To completely uninstall wp-env tool:
-
-```
-npm -g uninstall @wordpress/env
-```
-
-- To uninstall Docker, or Visual Studio Code use your OS method to remove packages. For example, on Windows run "Add or remove programs". You can additionally uninstall from the Docker Desktop app, click the bug icon at the top to switch to this Troubleshoot screen. Click Uninstall or remove.
-
-![Docker Troubleshoot Screenshot](https://developer.wordpress.org/files/2020/08/docker-uninstall-screen.png)
-
-### Uninstall Node/NVM
-
-To uninstall Node/NVM, delete the NVM directory, this is typically installed at `$HOME/.nvm`, delete using
-
-```
-rm -rf "$HOME/.nvm"
-```
-
-If this does not work and the `$NVM_DIR` environment variable is set you can remove using `rm -rf "$NVM_DIR"`
-
-To clean up any installed JavaScript packages remove the global `.npm` directory at `$HOME/.npm`,
-
-```
-rm -rf "$HOME/.npm"
-```
-
-Just as you confirmed the installation worked, you can confirm the uninstall worked. First quit and restart terminal and then try to run `npm -v`, `node -v`, and `nvm -v` you should then see a "command not found" error in the terminal.
+Of the two, `wp-env` is the more solid and complete solution. It's also the recommended tool for Gutenberg development. On the other hand, `wp-now` offers a simplified setup but is more limited than `wp-env`. Both are valid options, so the choice is yours.
diff --git a/docs/getting-started/devenv/docker-ubuntu.md b/docs/getting-started/devenv/docker-ubuntu.md
deleted file mode 100644
index 680798d74f4e8..0000000000000
--- a/docs/getting-started/devenv/docker-ubuntu.md
+++ /dev/null
@@ -1,60 +0,0 @@
-# How to setup local WordPress environment on Ubuntu
-
-This article covers setting up the local WordPress development environment using Docker on Ubuntu.
-
-For Ubuntu 20.04.1, the standard docker binaries in the repository work as needed:
-
-```
-sudo apt install docker.io docker-compose
-```
-
-For earlier versions of Ubuntu, the docker binaries included in repositories did not support the features needed for the WordPress environment.
-
-- For Ubuntu prior to 20.04.1, follow these [directions from Docker to install](https://docs.docker.com/install/linux/docker-ce/ubuntu/). Additionally `docker-compose` is required, you may need to install separately, see [ Docker compose documentation](https://docs.docker.com/compose/install/).
-
-## Troubleshooting
-
-If you run into this error, when running `npm run wp-env` from the Gutenberg directory:
-
-```
-ERROR: Couldn't connect to Docker daemon at http+docker://localhost - is it running?
-
-If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable.
-```
-
-First, make sure docker is running. You can check using `ps -ef | grep docker` which should show something like:
-
-```
-/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
-```
-
-If docker is not running, try to start the service using:
-
-```
-sudo systemctl start docker.service
-```
-
-If docker is running, then it is not listening how the WordPress environment is trying to communicate. Try adding the following service override file to include listening on tcp. See [this Docker documentation](https://docs.docker.com/config/daemon/remote-access/) on how to configure remote access for Docker daemon.
-
-```
-# /etc/systemd/system/docker.service.d/override.conf
-[Service]
-ExecStart=
-ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2376
-```
-
-Restart the service from the command-line
-
-```
-sudo systemctl daemon-reload
-sudo systemctl restart docker.service
-```
-
-After restarting the services, set the environment variable DOCKER_HOST and try starting using:
-
-```
-export DOCKER_HOST=tcp://127.0.0.1:2376
-npm run wp-env start
-```
-
-Your environment should be setup at: http://localhost:8888/
diff --git a/docs/getting-started/devenv/get-started-with-wp-env.md b/docs/getting-started/devenv/get-started-with-wp-env.md
new file mode 100644
index 0000000000000..f2c78c6fd236b
--- /dev/null
+++ b/docs/getting-started/devenv/get-started-with-wp-env.md
@@ -0,0 +1,140 @@
+# Get started with wp-env
+
+The [@wordpress/env](https://www.npmjs.com/package/@wordpress/env) package (`wp-env`) lets you set up a local WordPress environment (site) for building and testing plugins and themes, without any additional configuration.
+
+Before following this guide, install [Node.js development tools](/docs/getting-started/devenv#node-js-development-tools) if you have not already done so.
+
+## Quick start
+
+1. Download, install, and start [Docker Desktop](https://www.docker.com/products/docker-desktop) following the instructions for your operating system.
+2. Run `npm -g install @wordpress/env` in the terminal to install `wp-env` globally.
+3. In the terminal, navigate to an existing plugin directory, theme directory, or a new working directory.
+4. Run `wp-env start` in the terminal to start the local WordPress environment.
+5. After the script runs, navigate to `http://localhost:8888/wp-admin/` and log into the WordPress dashboard using username `admin` and password `password`.
+
+## Set up Docker Desktop
+
+The `wp-env` tool uses [Docker](https://www.docker.com/) to create a virtual machine that runs the local WordPress site. The Docker Desktop application is free for small businesses, personal use, education, and non-commercial open-source projects. See their [FAQ](https://docs.docker.com/desktop/faqs/general/#do-i-need-to-pay-to-use-docker-desktop) for more information.
+
+Use the links below to download and install Docker Desktop for your operating system.
+
+- [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/)
+- [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/)
+- [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/)
+
+If you are using a version of Ubuntu prior to 20.04.1, see the additional [troubleshooting notes](#ubuntu-docker-setup) below.
+
+After successful installation, start the Docker Desktop application and follow the prompts to get set up. You should generally use the recommended (default) settings, and creating a Docker account is optional.
+
+## Install and run `wp-env`
+
+The `wp-env` tool is used to create a local WordPress environment with Docker. So, after you have set up Docker Desktop, open the terminal and install the `wp-env` by running the command:
+
+```sh
+npm -g install @wordpress/env
+```
+
+This will install the `wp-env` globally, allowing the tool to be run from any directory. To confirm it's installed and available, run `wp-env --version`, and the version number should appear.
+
+Next, navigate to an existing plugin directory, theme directory, or a new working directory in the terminal and run:
+
+```sh
+wp-env start
+```
+
+Once the script completes, you can access the local environment at: `http://localhost:8888`. Log into the WordPress dashboard using username `admin` and password `password`.
+
+
+ Some projects, like Gutenberg, include their own specific wp-env configurations, and the documentation might prompt you to run npm run start wp-env instead.
+
+
+For more information on controlling the Docker environment, see the [@wordpress/env package](/packages/env/README.md) readme.
+
+### Where to run `wp-env`
+
+The `wp-env` tool can run from practically anywhere. When using the script while developing a single plugin, `wp-env start` can mount and activate the plugin automatically when run from the directory containing the plugin. This also works for themes when run from the directory in which you are developing the theme.
+
+A generic WordPress environment will be created if you run `wp-env start` from a directory that is not a plugin or theme. The script will display the following warning, but ignore if this is your intention.
+
+```
+!! Warning: could not find a .wp-env.json configuration file and could not determine if 'DIR' is a WordPress installation, a plugin, or a theme.
+```
+
+You can also use the `.wp-env.json` configuration file to create an environment that works with multiple plugins and/or themes. See the [@wordpress/env package](/packages/env/README.md#wp-envjson) readme for more configuration details.
+
+### Uninstall or reset `wp-env`
+
+Here are a few instructions if you need to start over or want to remove what was installed.
+
+- If you just want to reset and clean the WordPress database, run `wp-env clean all`
+- To remove the local environment completely for a specific project, run `wp-env destroy`
+- To globally uninstall the `wp-env` tool, run `npm -g uninstall @wordpress/env`
+
+## Troubleshooting
+
+### Common errors
+
+When using `wp-env`, it's common to get the error: `Error while running docker-compose command`
+
+- Check that Docker Desktop is started and running.
+- Check Docker Desktop dashboard for logs, restart, or remove existing virtual machines.
+- Then try rerunning `wp-env start`.
+
+If you see the error: `Host is already in use by another container`
+
+- The container you are attempting to start is already running, or another container is. You can stop an existing container by running `wp-env stop` from the directory that you started it in.
+- If you do not remember the directory where you started `wp-env`, you can stop all containers by running `docker stop $(docker ps -q)`. This will stop all Docker containers, so use with caution.
+- Then try rerunning `wp-env start`.
+
+### Ubuntu Docker setup
+
+If you are using a version of Ubuntu prior to 20.04.1, you may encounter errors when setting up a local WordPress environment with `wp-env`.
+
+To resolve this, start by following the [installation guide](https://docs.docker.com/install/linux/docker-ce/ubuntu/) from Docker. `docker-compose` is also required, which you may need to install separately. Refer to the [Docker compose documentation](https://docs.docker.com/compose/install/).
+
+Once Docker and `wp-env` are installed, and assuming `wp-env` is configured globally, try running `wp-env start` in a directory. If you run into this error:
+
+```
+ERROR: Couldn't connect to Docker daemon at http+docker://localhost - is it running?
+
+If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable.
+```
+
+First, make sure Docker is running. You can check by running `ps -ef | grep docker`, which should return something like:
+
+```
+/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
+```
+
+If Docker is not running, try starting the service by running `sudo systemctl start docker.service`.
+
+If Docker is running, then it is not listening to how the WordPress environment is trying to communicate. Try adding the following service override file to include listening on `tcp`. See [this Docker documentation](https://docs.docker.com/config/daemon/remote-access/) on how to configure remote access for Docker daemon.
+
+```
+# /etc/systemd/system/docker.service.d/override.conf
+[Service]
+ExecStart=
+ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2376
+```
+
+Restart the service from the command-line:
+
+```
+sudo systemctl daemon-reload
+sudo systemctl restart docker.service
+```
+
+After restarting the services, set the environment variable `DOCKER_HOST` and try starting `wp-env` with:
+
+```
+export DOCKER_HOST=tcp://127.0.0.1:2376
+wp-env start
+```
+
+Your environment should now be set up at `http://localhost:8888/`.
+
+## Additional resources
+
+- [@wordpress/env](https://www.npmjs.com/package/@wordpress/env) (Official documentation)
+- [Docker Desktop](https://docs.docker.com/desktop) (Official documentation)
+- [Quick and easy local WordPress development with wp-env](https://developer.wordpress.org/news/2023/03/quick-and-easy-local-wordpress-development-with-wp-env/) (WordPress Developer Blog)
diff --git a/docs/getting-started/devenv/get-started-with-wp-now.md b/docs/getting-started/devenv/get-started-with-wp-now.md
new file mode 100644
index 0000000000000..f4549735fcbdc
--- /dev/null
+++ b/docs/getting-started/devenv/get-started-with-wp-now.md
@@ -0,0 +1,66 @@
+# Get started with wp-now
+
+The [@wp-now/wp-now](https://www.npmjs.com/package/@wordpress/env) package (`wp-now`) is a lightweight tool powered by [WordPress Playground](https://developer.wordpress.org/playground/) that streamlines setting up a local WordPress environment.
+
+Before following this guide, install [Node.js development tools](/docs/getting-started/devenv#node-js-development-tools) if you have not already done so. It's recommended that you use the latest version of `node`. `wp-now` requires at least `node` v18 and v20 if you intend to use its [Blueprints](https://github.com/WordPress/playground-tools/tree/trunk/packages/wp-now#using-blueprints) feature.
+
+## Quick start
+
+1. Run `npm -g install @wp-now/wp-now` in the terminal to install `wp-now` globally.
+2. In the terminal, navigate to an existing plugin directory, theme directory, or a new working directory.
+3. Run `wp-now start` in the terminal to start the local WordPress environment.
+4. After the script runs, your default web browser will automatically open the new local site, and you'll be logged in with the username `admin` and the password `password`.
+
+## Install and run `wp-now`
+
+Under the hood, `wp-now` is powered by WordPress Playground and only requires Node.js, unlike `wp-env`, which also requires Docker. To install `wp-now`, open the terminal and run the command:
+
+```sh
+npm -g install @wp-now/wp-now
+```
+
+This will install the `wp-now` globally, allowing the tool to be run from any directory. To confirm it's installed and available, run `wp-now --version`, and the version number should appear.
+
+Next, navigate to an existing plugin directory, theme directory, or a new working directory in the terminal and run:
+
+```sh
+wp-now start
+```
+
+After the script runs, your default web browser will automatically open the new local site, and you'll be logged in with the username `admin` and the password `password`.
+
+
+ If you encounter any errors when running wp-now start, make sure that you are using at least node v18, or v20 if you are using the Blueprint feature.
+
+
+When running `wp-now` you can also pass arguments that modify the behavior of the WordPress environment. For example, `wp-now start --wp=6.3 --php=8` will start a site running WordPress 6.3 and PHP 8, which can be useful for testing purposes.
+
+Refer to the [@wp-now/wp-now](https://github.com/WordPress/playground-tools/tree/trunk/packages/wp-now) documentation for all available arguments.
+
+### Where to run `wp-now`
+
+The `wp-now` tool can be used practically anywhere and has different modes depending on how the directory is set up when you run `wp-now start`. Despite the many options, when developing for the Block Editor, you will likely use:
+
+- `plugin`, `theme`, or `wp-content`: Loads the project files into a virtual filesystem with WordPress and a SQLite-based database. Everything (including WordPress core files, the database, wp-config.php, etc.) is stored in the user's home directory and loaded into the virtual filesystem. The mode will be determined by:
+ - `plugin`: Presence of a PHP file with 'Plugin Name:' in its contents.
+ - `theme`: Presence of a `style.css` file with 'Theme Name:' in its contents.
+ - `wp-content`: Presence of `/plugins` and `/themes` subdirectories.
+- `playground`: If no other mode conditions are matched, `wp-now` launches a completely virtualized WordPress site.
+
+Refer to the [@wp-now/wp-now](https://github.com/WordPress/playground-tools/tree/trunk/packages/wp-now) documentation for a more detailed explanation of all modes.
+
+### Known issues
+
+Since `wp-now` is a relatively new tool, there are a few [known issues](https://github.com/WordPress/playground-tools/tree/trunk/packages/wp-now#known-issues) to be aware of. However, these issues are unlikely to impact most block, theme, or plugin development.
+
+### Uninstall or reset `wp-now`
+
+Here are a few instructions if you need to start over or want to remove what was installed.
+
+- If you just want to reset and clean the WordPress database, run `wp-now --reset`
+- To globally uninstall the `wp-now` tool, run `npm -g uninstall @wp-now/wp-now`
+
+## Additional resources
+
+- [@wp-now/wp-now](https://github.com/WordPress/playground-tools/tree/trunk/packages/wp-now) (Official documentation)
+- [WordPress Playground](https://developer.wordpress.org/playground/) (Developer overview)
\ No newline at end of file
diff --git a/docs/getting-started/devenv/nodejs-development-environment.md b/docs/getting-started/devenv/nodejs-development-environment.md
new file mode 100644
index 0000000000000..6e1d3638f5f03
--- /dev/null
+++ b/docs/getting-started/devenv/nodejs-development-environment.md
@@ -0,0 +1,50 @@
+# Node.js development environment
+
+When developing for the Block Editor, you will need [Node.js](https://nodejs.org/en) development tools along with a code editor and a local WordPress environment (see [Block Development Environment](/docs/getting-started/devenv/README.md)). Node.js (`node`) is an open-source runtime environment that allows you to execute JavaScript code from the terminal (also known as a command-line interface, CLI, or shell)
+
+Installing `node` will automatically include the Node Package Manager (`npm`) and the Node Package eXecute (`npx`), two tools you will frequently use in block and plugin development.
+
+Node Package Manager ([`npm`](https://docs.npmjs.com/cli/v10/commands/npm)) serves multiple purposes, including dependency management and script execution. It's the recommended package manager and is extensively featured in all documentation.
+
+The Node Package eXecute ([`npx`](https://docs.npmjs.com/cli/v10/commands/npx)) tool is used to run commands from packages without installing them globally and is commonly used when scaffolding blocks with the [`create-block`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/) package.
+
+
+## Node.js installation on Mac and Linux (with `nvm`)
+
+It's recommended that you use [Node Version Manager](https://github.com/nvm-sh/nvm) (`nvm`) to install Node.js. This allows you to install and manage specific versions of `node`, which are installed locally in your home directory, avoiding any global permission issues.
+
+Here are the quick instructions for installing `node` using `nvm` and setting the recommended Node.js version for block development. See the [complete installation guide](https://github.com/nvm-sh/nvm#installing-and-updating) for more details.
+
+1. Open the terminal and run the following to install `nvm`. On macOS, the required developer tools are not installed by default. Install them if prompted.
+
+```sh
+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.
+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.
+
+Some projects, like Gutenberg, include an [`.nvmrc`](https://github.com/WordPress/gutenberg/blob/trunk/.nvmrc) file which specifies the version of `node` that should be used. In this case, running `nvm use` will automatically select the correct version. If the version is not yet installed, you will get an error that tells you what version needs to be added. Run `nvm install [version-number]` followed by `nvm use`.
+
+## Node.js installation on Windows and others
+
+You can [download a Node.js installer](https://nodejs.org/en/download/) directly from the main Node.js website. The latest version is recommended. Installers are available for Windows and Mac, and binaries are available for Linux.
+
+Microsoft also provides a [detailed guide](https://learn.microsoft.com/en-us/windows/dev-environment/javascript/nodejs-on-windows#install-nvm-windows-nodejs-and-npm) on how to install `nvm` and Node.js on Windows and WSL.
+
+## Troubleshooting
+
+If you encounter the error `zsh: command not found: nvm` when attempting to install `node`, you might need to create the default profile file.
+
+The default shell is `zsh` on macOS, so create the profile file by running `touch ~/.zshrc` in the terminal. It's fine to run if the file already exists. The default profile is `bash` for Ubuntu, including WSL, so use `touch ~/.bashrc` instead. Then repeat steps 2-4.
+
+The latest `node` version should work for most development projects, but be aware that some packages and tools have specific requirements. If you encounter issues, you might need to install and use a previous `node` version. Also, make sure to check if the project has an `.nvmrc` and use the `node` version indicated.
+
+## Additional resources
+
+- [Node.js](https://nodejs.org/en) (Official documentation)
+- [Node Version Manager](https://github.com/nvm-sh/nvm) (Official documentation)
+- [Installing Node.js and npm for local WordPress development](https://learn.wordpress.org/tutorial/installing-node-js-and-npm-for-local-wordpress-development/) (Learn WordPress tutorial)
\ No newline at end of file
diff --git a/docs/explanations/faq.md b/docs/getting-started/faq.md
similarity index 99%
rename from docs/explanations/faq.md
rename to docs/getting-started/faq.md
index af7ed4f2e084b..a780be3fa940f 100644
--- a/docs/explanations/faq.md
+++ b/docs/getting-started/faq.md
@@ -377,7 +377,7 @@ Blocks are able to provide base structural CSS styles, and themes can add styles
Other features, like the new _wide_ and _full-wide_ alignment options, are simply CSS classes applied to blocks that offer this alignment. We are looking at how a theme can opt in to this feature, for example using `add_theme_support`.
-This is currently a work in progress and we recommend reviewing the [block based theme documentation](/docs/how-to-guides/themes/block-theme-overview.md) to learn more.
+This is currently a work in progress and we recommend reviewing the [block based theme documentation](https://developer.wordpress.org/themes/block-themes/) to learn more.
## What are block variations? Are they the same as block styles?
diff --git a/docs/getting-started/full-site-editing.md b/docs/getting-started/full-site-editing.md
index 72d85ec7e1367..ea24a659ab731 100644
--- a/docs/getting-started/full-site-editing.md
+++ b/docs/getting-started/full-site-editing.md
@@ -27,12 +27,9 @@ An ongoing [FSE Outreach program](https://make.wordpress.org/test/handbook/full-
## Block Themes
-If you are using the Gutenberg plugin you can run, test, and develop block themes. Block themes are themes built using templates composed using blocks. See [block theme overview](/docs/how-to-guides/themes/block-theme-overview.md) for additional details.
-
-- See the [Create a Block Theme](/docs/how-to-guides/themes/create-block-theme.md) tutorial for a walk-through of the pieces of a block theme.
+Block themes are themes built using templates composed using blocks. See the [block theme documentation](https://developer.wordpress.org/themes/block-themes/) for additional details.
- For examples, see the [WordPress/theme-experiments](https://github.com/WordPress/theme-experiments/) repository with several block themes there including the source for the above mentioned TT1 Blocks.
-
- Use the `empty-theme.php` script from theme-experiments repo to generate a starter block theme, it will prompt you with a few questions and create a theme.
```
diff --git a/docs/explanations/glossary.md b/docs/getting-started/glossary.md
similarity index 100%
rename from docs/explanations/glossary.md
rename to docs/getting-started/glossary.md
diff --git a/docs/getting-started/outreach.md b/docs/getting-started/outreach.md
deleted file mode 100644
index 9b51854f58926..0000000000000
--- a/docs/getting-started/outreach.md
+++ /dev/null
@@ -1,87 +0,0 @@
-# Outreach
-
-This includes articles, talks, demos and anything the community is doing to discuss, learn about, and contribute to Gutenberg. This is not an exhaustive list; if we are missing your event or article, just let us know in the [#core-editor slack channel](https://make.wordpress.org/chat/).
-
-## Articles
-
-A short list of useful articles around defining, extending, and contributing to Gutenberg.
-
-### Overviews of Gutenberg
-
-- [Status Check: Site Editing & Customization](https://make.wordpress.org/core/2020/12/10/status-check-site-editing-and-customization/), MatĆas Ventura Bausero (December 2020)
-- [Embrace the Modularity](https://riad.blog/2020/01/28/embrace-the-modularity/), Riad Benguella (January 2020)
-- [The Language of Gutenberg](https://lamda.blog/2018/04/22/the-language-of-gutenberg/), Miguel Fonseca (April 2018)
-- [Gutenberg, or the Ship of Theseus](https://matiasventura.com/post/gutenberg-or-the-ship-of-theseus/), MatĆas Ventura Bausero (October 2017)
-- [We Called It Gutenberg for a Reason](https://ma.tt/2017/08/we-called-it-gutenberg-for-a-reason/), Matt Mullenweg (August 2017)
-- [How Gutenberg is Changing WordPress Development](https://riad.blog/2017/10/06/how-gutenberg-is-changing-wordpress-development/), Riad Benguella (October 2017)
-- [How Gutenberg Will Shape the Future of WordPress](https://www.linkedin.com/pulse/gutenberg-morten-rand-hendriksen/), Morten Rand-Henrikson (August 2017)
-
-You can view this [Index of Gutenberg related posts](https://make.wordpress.org/core/handbook/references/keeping-up-with-gutenberg-index/) for more information.
-
-### Extending Gutenberg
-
-- [How to Start Block Development with Scaffolding](https://gziolo.pl/2020/12/22/how-to-start-block-development-with-scaffolding/), Grzegorz ZiĆ³Åkowski (December 2020)
-- [Introducing BlockBook for WordPress](https://riad.blog/2020/07/22/introducing-blockbook-for-wordpress/), Riad Benguella (July 2020)
-- [AsBlocks: an encrypted collaborative environment](https://riad.blog/2020/06/11/write-as-blocks-in-an-encrypted-collaborative-environment/), Riad Benguella (June 2020)
-- [Thoughts on Themes](https://matiasventura.com/post/thoughts-on-themes/), MatĆas Ventura Bausero (April 2020)
-- [Build a Block Series](https://mkaz.blog/code/build-a-block-series-1/), Marcus Kazmierczak (January 2020)
-- [With Gutenberg, what happens to my Custom Fields?](https://riad.blog/2017/12/11/with-gutenberg-what-happens-to-my-custom-fields/), Riad Benguella (December 2017)
-- [One thousand and one ways to extend Gutenberg today](https://riad.blog/2017/10/16/one-thousand-and-one-way-to-extend-gutenberg-today/), Riad Benguella (October 2017)
-- [Gutenberg Plugin Boilerplate](https://github.com/ahmadawais/Gutenberg-Boilerplate/), Ahmad Awais (August 2017)
-
-### Community Contribution
-
-- [The WordPress block editor: a maintainerās story](https://riad.blog/2020/10/26/the-wordpress-block-editor-a-maintainers-story/), Riad Benguella (October 2020)
-- [Good first issue on Gutenberg](https://mkaz.blog/code/good-first-issue-on-gutenberg/), Marcus Kazmierczak (August 2020)
-- [How to Use the New WordPress Block Editor](https://www.codeinwp.com/blog/wordpress-gutenberg-guide/), Colin Newcomer (July 2020)
-- [WordPress Gutenberg Developerās Guide](https://awhitepixel.com/guides/wordpress-gutenberg-developers-guide/), A White Pixel (2020)
-- [Gutenberg Block Library](https://editorblockswp.com/library), Danny Cooper (August 2018)
-- [A zero-configuration developer toolkit for building WordPress Gutenberg block plugins](https://ahmadawais.com/create-guten-block-toolkit/), Ahmad Awais (January 2018)
-- [Contributing to Gutenberg Without Code](https://wordimpress.com/a-pot-stirrer-amongst-chefs-contributing-to-gutenberg-without-code/), Kevin Hoffman (August 2017)
-- [Testing Flow in Gutenberg: Instructions for how to contribute to usability testing](https://make.wordpress.org/test/2017/11/22/testing-flow-in-gutenberg/), Anna Harrison (November 2017)
-
-### Article Compilations
-
-- [Full-Site-Editing: MVP and Ultimate Resource List](https://gutenbergtimes.com/full-site-editing/), Birgit Pauli-Haack (February 2021)
-- [Theme Shaper posts about Block Themes](https://themeshaper.com/tag/block-based-themes/), various authors (2020-2021)
-- [Gutenberg Times Updates](https://gutenbergtimes.com/category/updates/), Birgit Pauli-Haack
-- [Curated Collection of Gutenberg Articles, Plugins, Blocks, Tutorials, etc](http://gutenberghub.com/), Munir Kamal
-- [Gutenberg articles on ManageWP.org](https://managewp.org/search?q=gutenberg)
-
-## Talks
-
-Talks given about Gutenberg, including slides and videos as they are available.
-
-### Slides
-
-- [Growing JavaScript Skills with WordPress](https://gziolo.pl/2019/07/15/growing-javascript-skills-with-wordpress/) at JavaScript for WordPress conference (July 2019)
-- [The new core WordPress editor](http://kimb.me/talk-bigwp-london-new-core-wordpress-editor/) at BigWP London (May 2017)
-- [Gutenberg Notes](http://haiku2.com/2017/09/bend-wordpress-meetup-gutenberg-notes/) at Bend WordPress Meetup (September 2017)
-- [Gutenberg and the Future of Content in WordPress](https://www.slideshare.net/andrewmduthie/gutenberg-and-the-future-of-content-in-wordpress) (September 2017)
-- [Head first into Gutenberg](https://speakerdeck.com/prtksxna/head-first-into-gutenberg) at the [WordPress Goa Meet-up](https://www.meetup.com/WordPressGoa/events/245275573/) (December 2017)
-- [Gutenberg : vers une approche plus fine du contenu](https://imathi.eu/2018/02/16/gutenberg-vers-une-approche-plus-fine-du-contenu/) at [WP Paris](https://wpparis.fr/) (February 2018)
-
-### Videos
-
-- [All `Gutenberg` tagged Talks at WordPress.tv](https://wordpress.tv/tag/gutenberg/)
-- [Themes of the Future](https://wordpress.tv/2021/01/21/eileen-violini-themes-of-the-future-the-new-frontier-of-gutenberg-block-based-themes-and-theme-development/), Eileen Violini (January 2021)
-- [Content Creation in WordPress using Gutenberg](https://wordpress.tv/2021/02/06/sayed-taqui-content-creation-in-wordpress-using-gutenberg/),
- Sayed Taqui (January 2021)
-- [Updates on WordPress Site-Editor (FSE) and Themes](https://www.youtube.com/watch?v=z-5OJq-OBjI&t), Gutenberg Times (January 2021)
-- [State of the Word 2020 FSE Demo](https://youtu.be/QI3qCoiuG3w?t=1279), Matt Mullenweg (December 2020)
-- [Full Site Editing! It's Coming, But Will Change How We Use WordPress?](https://www.youtube.com/watch?v=JHxsDSAImn0), WPCrafter.com (December 2020)
-- [Advanced Layouts with the Block Editor](https://wordpress.tv/2020/12/06/advanced-layouts-with-the-block-editor/), Mel Choyce (December 2020)
-- [State of the Word 2019 Gutenberg Demo](https://www.youtube.com/watch?v=LezbkeV059Q), Matt Mullenweg (December 2019)
-- [Beyond Gutenberg](https://wordpress.tv/2018/07/09/matias-ventura-beyond-gutenberg/), MatĆas Ventura Bausero (July 2018)
-- [Anatomy of a block: Gutenberg design patterns](https://wordpress.tv/2018/07/08/tammie-lister-anatomy-of-a-block-gutenberg-design-patterns/), Tammie Lister (July 2018)
-- [State of the Word 2017 Gutenberg Demo](https://youtu.be/XOY3ZUO6P0k?t=2100), Matt Mullenweg with demo by MatĆas Ventura Bausero (December 2017)
-- [Gutenberg is Coming (Donāt Be Afraid)](https://training.ithemes.com/webinar/gutenberg-is-coming-dont-be-afraid/), iThemes Training (September 2017)
-
-## Learn WordPress Courses
-
-You can access all courses [here](https://learn.wordpress.org/).
-
-- [Intro to Block Development: Build Your First Custom Block](https://learn.wordpress.org/course/introduction-to-block-development-build-your-first-custom-block/), Jonathan Bossenger (2023)
-- [Registering Block Patterns](https://learn.wordpress.org/workshop/registering-block-patterns/), Daisy Olsen (January 2021)
-- [Intro to Gutenberg Block Development](https://learn.wordpress.org/workshop/intro-to-gutenberg-block-development/), Jonathan Bossenger (August 2020)
-- [Intro to Publishing with the Block Editor](https://learn.wordpress.org/workshop/intro-to-publishing-with-the-block-editor/), Erica Varlese (August 2020)
diff --git a/docs/how-to-guides/enqueueing-assets-in-the-editor.md b/docs/how-to-guides/enqueueing-assets-in-the-editor.md
index 1cc8b6de9ab5e..df869328e84b9 100644
--- a/docs/how-to-guides/enqueueing-assets-in-the-editor.md
+++ b/docs/how-to-guides/enqueueing-assets-in-the-editor.md
@@ -1,47 +1,47 @@
# Enqueueing assets in the Editor
-This guide is designed to be the definitive reference for enqueueing assets (scripts and styles) in the Editor. The approaches outlined here represent the recommended practices but keep in mind that this resource will evolve as WordPress does. Updates are encouraged.
+This guide is designed to be the definitive reference for enqueueing assets (scripts and styles) in the Editor. The approaches outlined here represent the recommended practices but keep in mind that this resource will evolve as WordPress does. Updates are encouraged.
-As of WordPress 6.3, the Post Editor is iframed if all registered blocks have a [`Block API version 3`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/) or higher and no traditional metaboxes are registered. The Site Editor has always been iframed. This guide assumes you are looking to enqueue assets for the iframed Editor, but refer to the backward compatibility section below for additional considerations.
+As of WordPress 6.3, the Post Editor is iframed if all registered blocks have a [`Block API version 3`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/) or higher and no traditional metaboxes are registered. The Site Editor has always been iframed. This guide assumes you are looking to enqueue assets for the iframed Editor, but refer to the backward compatibility section below for additional considerations.
For more information about why the Editor is iframed, please revisit the post [Blocks in an iframed (template) editor](https://make.wordpress.org/core/2021/06/29/blocks-in-an-iframed-template-editor/).
## The Editor versus Editor content
-Before enqueueing assets in the Editor, you must first identify what you are trying to target.
+Before enqueueing assets in the Editor, you must first identify what you are trying to target.
-Do you want to add styling or JavaScript to the user-generated content (blocks) in the Editor? Or do you want to modify the Editor user interface (UI) components or interact with Editor APIs? This could include everything from creating custom block controls to registering block variations.
+Do you want to add styling or JavaScript to the user-generated content (blocks) in the Editor? Or do you want to modify the Editor user interface (UI) components or interact with Editor APIs? This could include everything from creating custom block controls to registering block variations.
-There are different hooks to use depending on the answers to these questions, and if you are building a block or a theme, there are additional approaches to consider. Refer to the designated sections below.
+There are different hooks to use depending on the answers to these questions, and if you are building a block or a theme, there are additional approaches to consider. Refer to the designated sections below.
## Scenarios for enqueing assets
### Editor scripts and styles
-Whenever you need to enqueue assets for the Editor itself (i.e. not the user-generated content), you should use the [`enqueue_block_editor_assets`](https://developer.wordpress.org/reference/hooks/enqueue_block_editor_assets/) hook coupled with the standard [`wp_enqueue_script`](https://developer.wordpress.org/reference/functions/wp_enqueue_script/) and [`wp_enqueue_style`](https://developer.wordpress.org/reference/functions/wp_enqueue_style/) functions.
+Whenever you need to enqueue assets for the Editor itself (i.e. not the user-generated content), you should use the [`enqueue_block_editor_assets`](https://developer.wordpress.org/reference/hooks/enqueue_block_editor_assets/) hook coupled with the standard [`wp_enqueue_script`](https://developer.wordpress.org/reference/functions/wp_enqueue_script/) and [`wp_enqueue_style`](https://developer.wordpress.org/reference/functions/wp_enqueue_style/) functions.
-Examples might be adding custom inspector or toolbar controls, registering block styles and variations in Javascript, registering Editor plugins, etc.
+Examples might be adding custom inspector or toolbar controls, registering block styles and variations in Javascript, registering Editor plugins, etc.
```php
/**
* Enqueue Editor assets.
*/
function example_enqueue_editor_assets() {
- wp_enqueue_script(
- 'example-editor-scripts',
- plugins_url( 'editor-scripts.css', __FILE__ ) );
- }
- wp_enqueue_style(
- 'example-editor-styles',
- plugins_url( 'editor-styles.css', __FILE__ ) );
- }
+ wp_enqueue_script(
+ 'example-editor-scripts',
+ plugins_url( 'editor-scripts.js', __FILE__ )
+ );
+ wp_enqueue_style(
+ 'example-editor-styles',
+ plugins_url( 'editor-styles.css', __FILE__ )
+ );
}
-add_action( 'enqueue_block_editor_assets, 'example_enqueue_editor_assets' );
+add_action( 'enqueue_block_editor_assets', 'example_enqueue_editor_assets' );
```
While not the recommended approach, it's important to note that `enqueue_block_editor_assets` can be used to style Editor content for backward compatibility. See below for more details.
### Editor content scripts and styles
-As of WordPress 6.3, all assets added through the [`enqueue_block_assets`](https://developer.wordpress.org/reference/hooks/enqueue_block_assets/) PHP action will also be enqueued in the iframed Editor. See [#48286](https://github.com/WordPress/gutenberg/pull/48286) for more details.
+As of WordPress 6.3, all assets added through the [`enqueue_block_assets`](https://developer.wordpress.org/reference/hooks/enqueue_block_assets/) PHP action will also be enqueued in the iframed Editor. See [#48286](https://github.com/WordPress/gutenberg/pull/48286) for more details.
This is the primary method you should use to enqueue assets for user-generated content (blocks), and this hook fires both in the Editor and on the front end of your site. It should not be used to add assets intended for the Editor UI or to interact with Editor APIs. See below for a note on backward compatibility.
@@ -53,20 +53,20 @@ There are instances where you may only want to add assets in the Editor and not
*/
function example_enqueue_editor_content_assets() {
if ( is_admin() ) {
- wp_enqueue_script(
- 'example-editor-content-scripts',
- plugins_url( 'constent-scripts.css', __FILE__ ) );
- }
- wp_enqueue_style(
- 'example-editor-content-styles',
- plugins_url( 'constent-styles.css', __FILE__ ) );
- }
+ wp_enqueue_script(
+ 'example-editor-content-scripts',
+ plugins_url( 'constent-scripts.css', __FILE__ )
+ );
+ wp_enqueue_style(
+ 'example-editor-content-styles',
+ plugins_url( 'constent-styles.css', __FILE__ )
+ );
}
}
-add_action( 'enqueue_block_assets, 'example_enqueue_editor_content_assets' );
+add_action( 'enqueue_block_assets', 'example_enqueue_editor_content_assets' );
```
-You can also use the hook [`block_editor_settings_all`](https://developer.wordpress.org/reference/hooks/block_editor_settings_all/) to modify Editor settings directly. This method is a bit more complicated to implement but provides greater flexibility. It should only be used if `enqueue_block_assets` does not meet your needs.
+You can also use the hook [`block_editor_settings_all`](https://developer.wordpress.org/reference/hooks/block_editor_settings_all/) to modify Editor settings directly. This method is a bit more complicated to implement but provides greater flexibility. It should only be used if `enqueue_block_assets` does not meet your needs.
The following example sets the default text color for all paragraphs to `green`.
@@ -76,14 +76,14 @@ The following example sets the default text color for all paragraphs to `green`.
*
* @param array $editor_settings An array containing the current Editor settings.
* @param string $editor_context The context of the editor.
- *
+ *
* @return array Modified editor settings with the added custom CSS style.
*/
function example_modify_editor_settings( $editor_settings, $editor_context ) {
$editor_settings["styles"][] = array(
"css" => 'p { color: green }'
);
-
+
return $editor_settings;
}
add_filter( 'block_editor_settings_all', 'example_modify_editor_settings', 10,2 );
@@ -99,20 +99,22 @@ Beginning in WordPress 6.3, you can also use this method of modifying Editor set
### Block scripts and styles
-When building a block, `block.json` is the recommended way to enqueue all scripts and styles that are specifically required for the block itself. You are able to enqueue assets for the Editor, the front end, or both. See the [Block Metadata](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/) article for more details.
+When building a block, `block.json` is the recommended way to enqueue all scripts and styles that are specifically required for the block itself. You are able to enqueue assets for the Editor, the front end, or both. See the [Block Metadata](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/) article for more details.
### Theme scripts and styles
-If you need to enqueue Editor JavaScript in a theme, you can use either `enqueue_block_assets` or `enqueue_block_editor_assets` as outlined above. Editor-specific stylesheets should almost always be added with [`add_editor_style()`](https://developer.wordpress.org/reference/functions/add_editor_style/) or [`wp_enqueue_block_style()`](https://developer.wordpress.org/reference/functions/wp_enqueue_block_style/).
+If you need to enqueue Editor JavaScript in a theme, you can use either `enqueue_block_assets` or `enqueue_block_editor_assets` as outlined above. Editor-specific stylesheets should almost always be added with [`add_editor_style()`](https://developer.wordpress.org/reference/functions/add_editor_style/) or [`wp_enqueue_block_style()`](https://developer.wordpress.org/reference/functions/wp_enqueue_block_style/).
The `wp_enqueue_block_style()` function allows you to load per-block stylesheets in the Editor and on the front end. Coupled with `theme.json`, this is one of the best methods of styling blocks. See the WordPress Developer Blog article [Leveraging theme.json and per-block styles for more performant themes](https://developer.wordpress.org/news/2022/12/leveraging-theme-json-and-per-block-styles-for-more-performant-themes/) for more details.
## Backward compatibility and known issues
+As a general rule, when you enqueue assets in the iframed Editor, they will also be enqueued when the Editor is not iframed so long as you are using WordPress 6.3+. The opposite is not always true.
+
Suppose you are building a plugin or theme that requires backward compatibility to 6.2 or lower while maintaining compatibility with WordPress 6.3. In that case, you will not be able to use `enqueue_block_assets` since this hook does not enqueue assets in the content of the iframed Editor prior to 6.3.
As an alternative, you can use `enqueue_block_editor_assets` so long as the enqueued stylesheet contains at least one of the following selectors: `.editor-styles-wrapper`, `.wp-block`, or `.wp-block-*`. A warning message will be logged in the console, but the hook will apply the styles to the content of the Editor.
-Itās also important to note that as of WordPress 6.3, assets enqueued by `enqueue_block_assets` are loaded both inside and outside Editor's iframe for backward compatibility. Depending on the script libraries that you are trying to enqueue, this might cause problems. An ongoing discussion about this approach is happening in the Gutenberg [GitHub repository](https://github.com/WordPress/gutenberg/issues/53590).
+Itās also important to note that as of WordPress 6.3, assets enqueued by `enqueue_block_assets` are loaded both inside and outside Editor's iframe for backward compatibility. Depending on the script libraries that you are trying to enqueue, this might cause problems. An ongoing discussion about this approach is happening in the Gutenberg [GitHub repository](https://github.com/WordPress/gutenberg/issues/53590).
-If you experience issues using any of the methods outlined in this guide that have not been previously reported, please [submit an issue](https://github.com/WordPress/gutenberg/issues/new/choose) on GitHub.
\ No newline at end of file
+If you experience issues using any of the methods outlined in this guide that have not been previously reported, please [submit an issue](https://github.com/WordPress/gutenberg/issues/new/choose) on GitHub.
diff --git a/docs/how-to-guides/platform/custom-block-editor.md b/docs/how-to-guides/platform/custom-block-editor.md
index 242eccf460563..bb2639ddf706f 100644
--- a/docs/how-to-guides/platform/custom-block-editor.md
+++ b/docs/how-to-guides/platform/custom-block-editor.md
@@ -332,12 +332,7 @@ return (
-
-
-
-
-
-
+
);
@@ -419,27 +414,6 @@ Here is roughly how this works together to render the list of blocks:
The `@wordpress/block-editor` package components are among the most complex and involved. Understanding them is crucial if you want to grasp how the editor functions at a fundamental level. Studying these components is strongly advised.
-### Utility components in the custom block editor
-
-Jumping back to your custom `` component, it is also worth noting the following "utility" components:
-
-```js
-// File: src/components/block-editor/index.js
-
-
- /* 1. */
-
- /* 2. */
-
-
-
-```
-
-These provide other important elements of functionality for the editor instance.
-
-1. [``](https://github.com/WordPress/gutenberg/blob/e38dbe958c04d8089695eb686d4f5caff2707505/packages/block-editor/src/components/keyboard-shortcuts/index.js) ā Enables and usage of keyboard shortcuts within the editor
-2. [``](https://github.com/WordPress/gutenberg/blob/e38dbe958c04d8089695eb686d4f5caff2707505/packages/block-editor/src/components/writing-flow/index.js) ā Handles selection, focus management, and navigation across blocks
-
## Reviewing the sidebar
Also within the render of the ``, is the `` component.
@@ -453,9 +427,7 @@ return (
/* <-- SIDEBAR */
-
- // snip
-
+
);
diff --git a/docs/how-to-guides/themes/README.md b/docs/how-to-guides/themes/README.md
index 2b2137ce49ce4..708ecba03d264 100644
--- a/docs/how-to-guides/themes/README.md
+++ b/docs/how-to-guides/themes/README.md
@@ -18,6 +18,5 @@ There isn't an FSE specific theme type. In WordPress > 5.9 FSE is enabled for an
**Contents**
-- [Block Theme Overview](/docs/how-to-guides/themes/block-theme-overview.md)
- [Global Settings (theme.json)](/docs/how-to-guides/themes/theme-json.md)
- [Theme Support](/docs/how-to-guides/themes/theme-support.md)
diff --git a/docs/how-to-guides/themes/block-theme-overview.md b/docs/how-to-guides/themes/block-theme-overview.md
deleted file mode 100644
index 48a3621aa0fd0..0000000000000
--- a/docs/how-to-guides/themes/block-theme-overview.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# Block Theme
-
-These features are experimental and part of the Gutenberg plugin.
-For block theme features supported by WordPress 5.9 see the [Theme Developer Handbook](https://developer.wordpress.org/themes/block-themes/).
-
-You can provide feedback in the weekly #core-editor chats, or #fse-outreach-experiment channels, or async using GitHub issues.
-
-## What is a block theme?
-
-A block theme is a WordPress theme with templates entirely composed of blocks so that in addition to the post content of the different post types (pages, posts, ...), the block editor can also be used to edit all areas of the site: headers, footers, sidebars, etc.
-
-## Global styles presets
-
-In addition to the default theme.json file, Block Themes can define multiple global styles presets for users to pick from. For example, a theme author might provide multiple theme color variations for the theme.
-
-To provide a global styles preset, themes can add multiple JSON files inside their `/styles` folder. Each one of these JSON file is a mini theme.json file containing `styles` and/or `settings` that overrides any of the default `theme.json` file settings or styles.
-
-**Example**
-
-```json
-// styles/red.json
-{
- styles: {
- colors: {
- text: 'red',
- background: 'white'
- }
- }
-}
-```
-
-```json
-// styles/dark.json
-{
- styles: {
- colors: {
- text: 'white',
- background: 'black'
- }
- }
-}
-```
diff --git a/docs/how-to-guides/themes/create-block-theme.md b/docs/how-to-guides/themes/create-block-theme.md
deleted file mode 100644
index 356be104bbf79..0000000000000
--- a/docs/how-to-guides/themes/create-block-theme.md
+++ /dev/null
@@ -1,4 +0,0 @@
-
-##This content has moved
-
-You can find documentation about how to create a block theme in the [Theme Developer Handbook](https://developer.wordpress.org/themes/block-themes/).
diff --git a/docs/how-to-guides/themes/theme-json.md b/docs/how-to-guides/themes/theme-json.md
index 418e076fa839a..09264be194e89 100644
--- a/docs/how-to-guides/themes/theme-json.md
+++ b/docs/how-to-guides/themes/theme-json.md
@@ -968,7 +968,7 @@ You can use `ref: "styles.color.background"` to re-use the style for a block:
```JSON
{
"color": {
- "text": { ref: "styles.color.background" }
+ "text": { "ref": "styles.color.background" }
}
}
```
diff --git a/docs/manifest.json b/docs/manifest.json
index 0da0e4f89a443..7ceebaf69a449 100644
--- a/docs/manifest.json
+++ b/docs/manifest.json
@@ -12,15 +12,27 @@
"parent": null
},
{
- "title": "Development Environment",
+ "title": "Block Development Environment",
"slug": "devenv",
"markdown_source": "../docs/getting-started/devenv/README.md",
"parent": "getting-started"
},
{
- "title": "How to setup local WordPress environment on Ubuntu",
- "slug": "docker-ubuntu",
- "markdown_source": "../docs/getting-started/devenv/docker-ubuntu.md",
+ "title": "Node.js development environment",
+ "slug": "nodejs-development-environment",
+ "markdown_source": "../docs/getting-started/devenv/nodejs-development-environment.md",
+ "parent": "devenv"
+ },
+ {
+ "title": "Get started with wp-env",
+ "slug": "get-started-with-wp-env",
+ "markdown_source": "../docs/getting-started/devenv/get-started-with-wp-env.md",
+ "parent": "devenv"
+ },
+ {
+ "title": "Get started with wp-now",
+ "slug": "get-started-with-wp-now",
+ "markdown_source": "../docs/getting-started/devenv/get-started-with-wp-now.md",
"parent": "devenv"
},
{
@@ -78,9 +90,15 @@
"parent": "getting-started"
},
{
- "title": "Outreach",
- "slug": "outreach",
- "markdown_source": "../docs/getting-started/outreach.md",
+ "title": "Glossary",
+ "slug": "glossary",
+ "markdown_source": "../docs/getting-started/glossary.md",
+ "parent": "getting-started"
+ },
+ {
+ "title": "Frequently Asked Questions",
+ "slug": "faq",
+ "markdown_source": "../docs/getting-started/faq.md",
"parent": "getting-started"
},
{
@@ -323,12 +341,6 @@
"markdown_source": "../docs/how-to-guides/themes/README.md",
"parent": "how-to-guides"
},
- {
- "title": "Block Theme",
- "slug": "block-theme-overview",
- "markdown_source": "../docs/how-to-guides/themes/block-theme-overview.md",
- "parent": "themes"
- },
{
"title": "Global Settings & Styles (theme.json)",
"slug": "theme-json",
@@ -1913,6 +1925,12 @@
"markdown_source": "../packages/token-list/README.md",
"parent": "packages"
},
+ {
+ "title": "@wordpress/undo-manager",
+ "slug": "packages-undo-manager",
+ "markdown_source": "../packages/undo-manager/README.md",
+ "parent": "packages"
+ },
{
"title": "@wordpress/url",
"slug": "packages-url",
@@ -1979,6 +1997,12 @@
"markdown_source": "../docs/reference-guides/data/data-core-blocks.md",
"parent": "data"
},
+ {
+ "title": "The Commands Data",
+ "slug": "data-core-commands",
+ "markdown_source": "../docs/reference-guides/data/data-core-commands.md",
+ "parent": "data"
+ },
{
"title": "Customize Widgets",
"slug": "data-core-customize-widgets",
@@ -2135,18 +2159,6 @@
"markdown_source": "../docs/explanations/user-interface/design-resources.md",
"parent": "user-interface"
},
- {
- "title": "Frequently Asked Questions",
- "slug": "faq",
- "markdown_source": "../docs/explanations/faq.md",
- "parent": "explanations"
- },
- {
- "title": "Glossary",
- "slug": "glossary",
- "markdown_source": "../docs/explanations/glossary.md",
- "parent": "explanations"
- },
{
"title": "History",
"slug": "history",
@@ -2291,12 +2303,6 @@
"markdown_source": "../docs/contributors/design/the-block.md",
"parent": "design"
},
- {
- "title": "Reference",
- "slug": "reference",
- "markdown_source": "../docs/contributors/design/reference.md",
- "parent": "design"
- },
{
"title": "Documentation Contributions",
"slug": "documentation",
diff --git a/docs/reference-guides/block-api/block-attributes.md b/docs/reference-guides/block-api/block-attributes.md
index eafc73c79938f..0fbbeeb13680e 100644
--- a/docs/reference-guides/block-api/block-attributes.md
+++ b/docs/reference-guides/block-api/block-attributes.md
@@ -305,39 +305,6 @@ Attribute available in the block:
{ "content": "The inner text of the figcaption element" }
```
-Use the `multiline` property to extract the inner HTML of matching tag names for the use in `RichText` with the `multiline` prop.
-
-_Example_: Extract the `content` attribute from a blockquote element found in the block's markup.
-
-Saved content:
-```html
-
" }
-```
-
### `query` source
Use `query` to extract an array of values from markup. Entries of the array are determined by the `selector` argument, where each matched element within the block will have an entry structured corresponding to the second argument, an object of attribute sources.
diff --git a/docs/reference-guides/block-api/block-edit-save.md b/docs/reference-guides/block-api/block-edit-save.md
index 843a8d5be12d3..f38573f35a5f2 100644
--- a/docs/reference-guides/block-api/block-edit-save.md
+++ b/docs/reference-guides/block-api/block-edit-save.md
@@ -385,7 +385,7 @@ edit: ( { attributes, setAttributes } ) => {
value={ attributes.content }
onChange={ updateFieldValue }
/>
-
+
);
},
@@ -465,7 +465,7 @@ edit: ( { attributes, setAttributes } ) => {
setAttributes( { postsToShow: parseInt( val ) } );
}}
/>
-
+
);
},
diff --git a/docs/reference-guides/block-api/block-metadata.md b/docs/reference-guides/block-api/block-metadata.md
index ba24ff0f58401..22d9d66b4f462 100644
--- a/docs/reference-guides/block-api/block-metadata.md
+++ b/docs/reference-guides/block-api/block-metadata.md
@@ -464,7 +464,7 @@ Plugins and Themes can also register [custom block style](/docs/reference-guides
It provides structured example data for the block. This data is used to construct a preview for the block to be shown in the Inspector Help Panel when the user mouses over the block.
-See the [the example documentation](/docs/reference-guides/block-api/block-registration.md#example-optional) for more details.
+See the [Example documentation](/docs/reference-guides/block-api/block-registration.md#example-optional) for more details.
### Variations
@@ -497,6 +497,25 @@ _Note: In JavaScript you can provide a function for the `isActive` property, and
See the [the variations documentation](/docs/reference-guides/block-api/block-variations.md) for more details.
+### Block Hooks
+
+- Type: `object`
+- Optional
+- Property: `blockHooks`
+- Since: `WordPress 6.4.0`
+
+```json
+{
+ "blockHooks": {
+ "my-plugin/banner": "after"
+ }
+}
+```
+
+Block Hooks is an API that allows a block to automatically insert itself next to all instances of a given block type, in a relative position also specified by the "hooked" block. That is, a block can opt to be inserted before or after a given block type, or as its first or last child (i.e. to be prepended or appended to the list of its child blocks, respectively). Hooked blocks will appear both on the frontend and in the editor (to allow for customization by the user).
+
+The key is the name of the block (`string`) to hook into, and the value is the position to hook into (`string`). Take a look at the [Block Hooks documentation](/docs/reference-guides/block-api/block-registration.md#block-hooks-optional) for more info about available configurations.
+
### Editor Script
- Type: `WPDefinedAsset`|`WPDefinedAsset[]` ([learn more](#wpdefinedasset))
diff --git a/docs/reference-guides/block-api/block-registration.md b/docs/reference-guides/block-api/block-registration.md
index cecdb663e3d60..0fcaaa510b81a 100644
--- a/docs/reference-guides/block-api/block-registration.md
+++ b/docs/reference-guides/block-api/block-registration.md
@@ -234,6 +234,7 @@ example: {
#### variations (optional)
- **Type:** `Object[]`
+- **Since**: `WordPress 5.9.0`
Similarly to how the block's styles can be declared, a block type can define block variations that the user can pick from. The difference is that, rather than changing only the visual appearance, this field provides a way to apply initial custom attributes and inner blocks at the time when a block is inserted. See the [Block Variations API](/docs/reference-guides/block-api/block-variations.md) for more details.
@@ -265,6 +266,7 @@ parent: [ 'core/columns' ],
#### ancestor (optional)
- **Type:** `Array`
+- **Since**: `WordPress 6.0.0`
The `ancestor` property makes a block available inside the specified block types at any position of the ancestor block subtree. That allows, for example, to place a 'Comment Content' block inside a 'Column' block, as long as 'Column' is somewhere within a 'Comment Template' block. In comparison to the `parent` property blocks that specify their `ancestor` can be placed anywhere in the subtree whilst blocks with a specified `parent` need to be direct children.
@@ -273,6 +275,35 @@ The `ancestor` property makes a block available inside the specified block types
ancestor: [ 'core/columns' ],
```
+#### Block Hooks (optional)
+
+- **Type:** `Object`
+- **Since**: `WordPress 6.4.0`
+
+Block Hooks is an API that allows a block to automatically insert itself next to all instances of a given block type, in a relative position also specified by the "hooked" block. That is, a block can opt to be inserted before or after a given block type, or as its first or last child (i.e. to be prepended or appended to the list of its child blocks, respectively). Hooked blocks will appear both on the frontend and in the editor (to allow for customization by the user).
+
+The key is the name of the block (`string`) to hook into, and the value is the position to hook into (`string`). Allowed target values are:
+
+- `before` ā inject before the target block.
+- `after` - inject after the target block.
+- `firstChild` - inject before the first inner block of the target container block.
+- `lastChild` - inject after the last inner block of the target container block.
+
+```js
+{
+ blockHooks: {
+ 'core/verse': 'before'
+ 'core/spacer': 'after',
+ 'core/column': 'firstChild',
+ 'core/group': 'lastChild',
+ }
+}
+```
+
+Itās crucial to emphasize that the Block Hooks feature is only designed to work with _static_ block-based templates, template parts, and patterns. For patterns, this includes those provided by the theme, from [Block Pattern Directory](https://wordpress.org/patterns/), or from calls to [`register_block_pattern`](https://developer.wordpress.org/reference/functions/register_block_pattern/).
+
+Block Hooks will not work with post content or patterns crafted by the user, such as synced patterns, or theme templates and template parts that have been modified by the user.
+
## Block Collections
## `registerBlockCollection`
diff --git a/docs/reference-guides/block-api/block-transforms.md b/docs/reference-guides/block-api/block-transforms.md
index 5efc76064fe31..4a6101df3394c 100644
--- a/docs/reference-guides/block-api/block-transforms.md
+++ b/docs/reference-guides/block-api/block-transforms.md
@@ -96,7 +96,7 @@ A transformation of type `enter` is an object that takes the following parameter
- **type** _(string)_: the value `enter`.
- **regExp** _(RegExp)_: the Regular Expression to use as a matcher. If the value matches, the transformation will be applied.
-- **transform** _(function)_: a callback that receives the value that has been entered. It should return a block object or an array of block objects.
+- **transform** _(function)_: a callback that receives an object with a `content` field containing the value that has been entered. It should return a block object or an array of block objects.
- **priority** _(number, optional)_: controls the priority with which a transform is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://codex.wordpress.org/Plugin_API#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set.
**Example: from --- to Separator block**
diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md
index 0f25cead8b07a..98a744e30cf02 100644
--- a/docs/reference-guides/core-blocks.md
+++ b/docs/reference-guides/core-blocks.md
@@ -2,10 +2,9 @@
This page lists the blocks included in the block-library package.
-- Items marked with a strikeout (~~strikeout~~) are explicitly disabled.
-- Blocks marked with **Experimental:** true are only available when Gutenberg is active.
-- Blocks marked with **Experimental:** fse are only available in the Site Editor.
-
+- Items marked with a strikeout (~~strikeout~~) are explicitly disabled.
+- Blocks marked with **Experimental:** true are only available when Gutenberg is active.
+- Blocks marked with **Experimental:** fse are only available in the Site Editor.
@@ -53,7 +52,7 @@ Prompt visitors to take action with a button-style link. ([Source](https://githu
- **Category:** design
- **Parent:** core/buttons
- **Supports:** anchor, color (background, gradients, text), shadow, spacing (padding), typography (fontSize, lineHeight), ~~alignWide~~, ~~align~~, ~~reusable~~
-- **Attributes:** backgroundColor, gradient, linkTarget, placeholder, rel, text, textAlign, textColor, title, url, width
+- **Attributes:** backgroundColor, gradient, linkTarget, placeholder, rel, tagName, text, textAlign, textColor, title, type, url, width
## Buttons
@@ -98,7 +97,7 @@ A single column within a columns block. ([Source](https://github.com/WordPress/g
- **Name:** core/column
- **Category:** design
- **Parent:** core/columns
-- **Supports:** anchor, color (background, gradients, heading, link, text), layout, spacing (blockGap, padding), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~
+- **Supports:** anchor, color (background, button, gradients, heading, link, text), layout, spacing (blockGap, padding), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~
- **Attributes:** allowedBlocks, templateLock, verticalAlignment, width
## Columns
@@ -107,7 +106,7 @@ Display content in multiple columns, with blocks added to each column. ([Source]
- **Name:** core/columns
- **Category:** design
-- **Supports:** align (full, wide), anchor, color (background, gradients, link, text), layout (default, ~~allowEditing~~, ~~allowInheriting~~, ~~allowSwitching~~), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~
+- **Supports:** align (full, wide), anchor, color (background, button, gradients, heading, link, text), layout (default, ~~allowEditing~~, ~~allowInheriting~~, ~~allowSwitching~~), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~
- **Attributes:** isStackedOnMobile, templateLock, verticalAlignment
## Comment Author Avatar (deprecated)
@@ -266,7 +265,7 @@ Add a link to a downloadable file. ([Source](https://github.com/WordPress/gutenb
- **Name:** core/file
- **Category:** media
-- **Supports:** align, anchor, color (background, gradients, link, ~~text~~), spacing (margin, padding)
+- **Supports:** align, anchor, color (background, gradients, link, ~~text~~), interactivity, spacing (margin, padding)
- **Attributes:** displayPreview, downloadButtonText, fileId, fileName, href, id, previewHeight, showDownloadButton, textLinkHref, textLinkTarget
## Footnotes
@@ -302,7 +301,7 @@ Gather blocks in a layout container. ([Source](https://github.com/WordPress/gute
- **Name:** core/group
- **Category:** design
-- **Supports:** align (full, wide), anchor, ariaLabel, color (background, gradients, heading, link, text), dimensions (minHeight), layout (allowSizingOnChildren), position (sticky), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~
+- **Supports:** align (full, wide), anchor, ariaLabel, background (backgroundImage), color (background, button, gradients, heading, link, text), dimensions (minHeight), layout (allowSizingOnChildren), position (sticky), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~
- **Attributes:** allowedBlocks, tagName, templateLock
## Heading
@@ -311,7 +310,7 @@ Introduce new sections and organize content to help visitors (and search engines
- **Name:** core/heading
- **Category:** text
-- **Supports:** align (full, wide), anchor, className, color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight)
+- **Supports:** __unstablePasteTextInline, align (full, wide), anchor, className, color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight)
- **Attributes:** content, level, placeholder, textAlign
## Home Link
@@ -339,8 +338,8 @@ Insert an image to make a visual statement. ([Source](https://github.com/WordPre
- **Name:** core/image
- **Category:** media
-- **Supports:** anchor, behaviors (lightbox), color (~~background~~, ~~text~~), filter (duotone)
-- **Attributes:** align, alt, aspectRatio, caption, height, href, id, linkClass, linkDestination, linkTarget, rel, scale, sizeSlug, title, url, width
+- **Supports:** anchor, color (~~background~~, ~~text~~), filter (duotone)
+- **Attributes:** align, alt, aspectRatio, caption, height, href, id, lightbox, linkClass, linkDestination, linkTarget, rel, scale, sizeSlug, title, url, width
## Latest Comments
@@ -421,7 +420,7 @@ A collection of blocks that allow visitors to get around your site. ([Source](ht
- **Name:** core/navigation
- **Category:** theme
-- **Supports:** align (full, wide), inserter, layout (allowSizingOnChildren, default, ~~allowInheriting~~, ~~allowSwitching~~, ~~allowVerticalAlignment~~), spacing (blockGap, units), typography (fontSize, lineHeight), ~~html~~
+- **Supports:** align (full, wide), ariaLabel, inserter, interactivity, layout (allowSizingOnChildren, default, ~~allowInheriting~~, ~~allowSwitching~~, ~~allowVerticalAlignment~~), spacing (blockGap, units), typography (fontSize, lineHeight), ~~html~~
- **Attributes:** __unstableLocation, backgroundColor, customBackgroundColor, customOverlayBackgroundColor, customOverlayTextColor, customTextColor, hasIcon, icon, maxNestingLevel, openSubmenusOnClick, overlayBackgroundColor, overlayMenu, overlayTextColor, ref, rgbBackgroundColor, rgbTextColor, showSubmenuIcon, templateLock, textColor
## Custom Link
@@ -563,7 +562,7 @@ Displays the contents of a post or page. ([Source](https://github.com/WordPress/
- **Name:** core/post-content
- **Category:** theme
-- **Supports:** align (full, wide), color (background, gradients, link, text), dimensions (minHeight), layout, typography (fontSize, lineHeight), ~~html~~
+- **Supports:** align (full, wide), color (background, gradients, link, text), dimensions (minHeight), layout, spacing (blockGap), typography (fontSize, lineHeight), ~~html~~
- **Attributes:**
## Date
@@ -759,7 +758,7 @@ Help visitors find your content. ([Source](https://github.com/WordPress/gutenber
- **Name:** core/search
- **Category:** widgets
-- **Supports:** align (center, left, right), color (background, gradients, text), typography (fontSize, lineHeight), ~~html~~
+- **Supports:** align (center, left, right), color (background, gradients, text), interactivity, typography (fontSize, lineHeight), ~~html~~
- **Attributes:** buttonBehavior, buttonPosition, buttonText, buttonUseIcon, isSearchFieldHidden, label, placeholder, query, showLabel, width, widthUnit
## Separator
diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md
index fbdf0c5dfd81b..7b0bd386daaf4 100644
--- a/docs/reference-guides/data/data-core-block-editor.md
+++ b/docs/reference-guides/data/data-core-block-editor.md
@@ -473,11 +473,11 @@ Returns an array containing the clientIds of all descendants of the blocks given
_Parameters_
- _state_ `Object`: Global application state.
-- _clientIds_ `Array`: Array of blocks to inspect.
+- _clientIds_ `string|string[]`: Client ID(s) for which descendant blocks are to be returned.
_Returns_
-- `Array`: ids of descendants.
+- `Array`: Client IDs of descendants.
### getClientIdsWithDescendants
@@ -1077,6 +1077,19 @@ _Returns_
- `boolean`: Whether block is first in multi-selection.
+### isGroupable
+
+Indicates if the provided blocks(by client ids) are groupable. We need to have at least one block, have a grouping block name set and be able to remove these blocks.
+
+_Parameters_
+
+- _state_ `Object`: Global application state.
+- _clientIds_ `string[]`: Block client ids. If not passed the selected blocks client ids will be used.
+
+_Returns_
+
+- `boolean`: True if the blocks are groupable.
+
### isLastBlockChangePersistent
Returns true if the most recent block change is be considered persistent, or false otherwise. A persistent change is one committed by BlockEditorProvider via its `onChange` callback, in addition to `onInput`.
@@ -1141,6 +1154,19 @@ _Returns_
- `boolean`: Whether user is typing.
+### isUngroupable
+
+Indicates if a block is ungroupable. A block is ungroupable if it is a single grouping block with inner blocks. If a block has an `ungroup` transform, it is also ungroupable, without the requirement of being the default grouping block. Additionally a block can only be ungrouped if it has inner blocks and can be removed.
+
+_Parameters_
+
+- _state_ `Object`: Global application state.
+- _clientId_ `string`: Client Id of the block. If not passed the selected block's client id will be used.
+
+_Returns_
+
+- `boolean`: True if the block is ungroupable.
+
### isValidTemplate
Returns whether the blocks matches the template or not.
diff --git a/docs/reference-guides/data/data-core-edit-post.md b/docs/reference-guides/data/data-core-edit-post.md
index b579e7e658007..7d6a1deed455b 100644
--- a/docs/reference-guides/data/data-core-edit-post.md
+++ b/docs/reference-guides/data/data-core-edit-post.md
@@ -490,6 +490,10 @@ _Parameters_
- _mode_ `string`: The editor mode.
+### toggleDistractionFree
+
+Action that toggles Distraction free mode. Distraction free mode expects there are no sidebars, as due to the z-index values set, you can't close sidebars.
+
### toggleEditorPanelEnabled
Returns an action object used to enable or disable a panel in the editor.
diff --git a/docs/reference-guides/data/data-core-edit-site.md b/docs/reference-guides/data/data-core-edit-site.md
index 0cac2268b2ab2..6dea8e9b77d1b 100644
--- a/docs/reference-guides/data/data-core-edit-site.md
+++ b/docs/reference-guides/data/data-core-edit-site.md
@@ -291,7 +291,7 @@ _Parameters_
_Returns_
-- `number`: The resolved template ID for the page route.
+- `Object`: Action object.
### setHasPageContentFocus
diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md
index f2bc3374f9e72..acaaa5f23f1b9 100644
--- a/docs/reference-guides/data/data-core.md
+++ b/docs/reference-guides/data/data-core.md
@@ -371,7 +371,7 @@ _Usage_
_Parameters_
-- _state_ `State`: Editor state.
+- _state_ Editor state.
_Returns_
@@ -403,6 +403,18 @@ _Returns_
- `Optional< any >`: The edit.
+### getUserPatternCategories
+
+Retrieve the registered user pattern categories.
+
+_Parameters_
+
+- _state_ `State`: Data state.
+
+_Returns_
+
+- `Array< UserPatternCategory >`: User patterns category array.
+
### getUserQueryResults
Returns all the users returned by a query ID.
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 9f9e4f68aedec..4890386ca8333 100644
--- a/docs/reference-guides/theme-json-reference/theme-json-living.md
+++ b/docs/reference-guides/theme-json-reference/theme-json-living.md
@@ -31,6 +31,7 @@ Code editors can pick up the schema and can provide helpful hints and suggestion
Setting that enables the following UI tools:
+- background: backgroundImage
- border: color, radius, style, width
- color: link
- dimensions: minHeight
@@ -97,6 +98,16 @@ Settings related to colors.
---
+### background
+
+Settings related to background.
+
+| Property | Type | Default | Props |
+| --- | --- | --- |--- |
+| backgroundImage | boolean | false | |
+
+---
+
### dimensions
Settings related to dimensions.
@@ -119,6 +130,17 @@ Settings related to layout.
---
+### lightbox
+
+Settings related to the lightbox.
+
+| Property | Type | Default | Props |
+| --- | --- | --- |--- |
+| enabled | boolean | | |
+| allowEditing | boolean | | |
+
+---
+
### position
Settings related to position.
@@ -172,16 +194,6 @@ Settings related to typography.
Generate custom CSS custom properties of the form `--wp--custom--{key}--{nested-key}: {value};`. `camelCased` keys are transformed to `kebab-case` as to follow the CSS property naming schema. Keys at different depth levels are separated by `--`, so keys should not include `--` in the name.
----
-
-### behaviors
-
-Settings related to behaviors.
-
-| Property | Type | Default | Props |
-| --- | --- | --- |--- |
-| lightbox | boolean | false | |
-
---
## Styles
diff --git a/docs/toc.json b/docs/toc.json
index d30b85ec36c1b..c9f24dd9fad70 100644
--- a/docs/toc.json
+++ b/docs/toc.json
@@ -7,7 +7,13 @@
{
"docs/getting-started/devenv/README.md": [
{
- "docs/getting-started/devenv/docker-ubuntu.md": []
+ "docs/getting-started/devenv/nodejs-development-environment.md": []
+ },
+ {
+ "docs/getting-started/devenv/get-started-with-wp-env.md": []
+ },
+ {
+ "docs/getting-started/devenv/get-started-with-wp-now.md": []
}
]
},
@@ -37,7 +43,8 @@
]
},
{ "docs/getting-started/full-site-editing.md": [] },
- { "docs/getting-started/outreach.md": [] }
+ { "docs/getting-started/glossary.md": [] },
+ { "docs/getting-started/faq.md": [] }
]
},
{
@@ -136,7 +143,6 @@
{ "docs/how-to-guides/propagating-updates.md": [] },
{
"docs/how-to-guides/themes/README.md": [
- { "docs/how-to-guides/themes/block-theme-overview.md": [] },
{ "docs/how-to-guides/themes/theme-json.md": [] },
{ "docs/how-to-guides/themes/theme-support.md": [] }
]
@@ -268,6 +274,9 @@
"docs/reference-guides/data/data-core-block-editor.md": []
},
{ "docs/reference-guides/data/data-core-blocks.md": [] },
+ {
+ "docs/reference-guides/data/data-core-commands.md": []
+ },
{
"docs/reference-guides/data/data-core-customize-widgets.md": []
},
@@ -321,8 +330,6 @@
}
]
},
- { "docs/explanations/faq.md": [] },
- { "docs/explanations/glossary.md": [] },
{ "docs/explanations/history.md": [] }
]
},
@@ -383,8 +390,7 @@
},
{
"docs/contributors/design/README.md": [
- { "docs/contributors/design/the-block.md": [] },
- { "docs/contributors/design/reference.md": [] }
+ { "docs/contributors/design/the-block.md": [] }
]
},
{
diff --git a/gutenberg.php b/gutenberg.php
index 44146b0c57d30..85a799110597b 100644
--- a/gutenberg.php
+++ b/gutenberg.php
@@ -3,9 +3,9 @@
* Plugin Name: Gutenberg
* Plugin URI: https://github.com/WordPress/gutenberg
* Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality.
- * Requires at least: 6.1
+ * Requires at least: 6.2
* Requires PHP: 7.0
- * Version: 16.6.0-rc.1
+ * Version: 16.7.0-rc.1
* Author: Gutenberg Team
* Text Domain: gutenberg
*
diff --git a/lib/block-supports/background.php b/lib/block-supports/background.php
new file mode 100644
index 0000000000000..a32ee72fd90c6
--- /dev/null
+++ b/lib/block-supports/background.php
@@ -0,0 +1,103 @@
+attributes ) {
+ $block_type->attributes = array();
+ }
+
+ // Check for existing style attribute definition e.g. from block.json.
+ if ( array_key_exists( 'style', $block_type->attributes ) ) {
+ return;
+ }
+
+ $has_background_support = block_has_support( $block_type, array( 'background' ), false );
+
+ if ( $has_background_support ) {
+ $block_type->attributes['style'] = array(
+ 'type' => 'object',
+ );
+ }
+}
+
+/**
+ * Renders the background styles to the block wrapper.
+ * This block support uses the `render_block` hook to ensure that
+ * it is also applied to non-server-rendered blocks.
+ *
+ * @param string $block_content Rendered block content.
+ * @param array $block Block object.
+ * @return string Filtered block content.
+ */
+function gutenberg_render_background_support( $block_content, $block ) {
+ $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
+ $block_attributes = $block['attrs'];
+ $has_background_image_support = block_has_support( $block_type, array( 'background', 'backgroundImage' ), false );
+
+ if (
+ ! $has_background_image_support ||
+ wp_should_skip_block_supports_serialization( $block_type, 'background', 'backgroundImage' )
+ ) {
+ return $block_content;
+ }
+
+ $background_image_source = $block_attributes['style']['background']['backgroundImage']['source'] ?? null;
+ $background_image_url = $block_attributes['style']['background']['backgroundImage']['url'] ?? null;
+ $background_size = $block_attributes['style']['background']['backgroundSize'] ?? 'cover';
+
+ $background_block_styles = array();
+
+ if (
+ 'file' === $background_image_source &&
+ $background_image_url
+ ) {
+ // Set file based background URL.
+ // TODO: In a follow-up, similar logic could be added to inject a featured image url.
+ $background_block_styles['backgroundImage']['url'] = $background_image_url;
+ // Only output the background size when an image url is set.
+ $background_block_styles['backgroundSize'] = $background_size;
+ }
+
+ $styles = gutenberg_style_engine_get_styles( array( 'background' => $background_block_styles ) );
+
+ if ( ! empty( $styles['css'] ) ) {
+ // Inject background styles to the first element, presuming it's the wrapper, if it exists.
+ $tags = new WP_HTML_Tag_Processor( $block_content );
+
+ if ( $tags->next_tag() ) {
+ $existing_style = $tags->get_attribute( 'style' );
+ $updated_style = '';
+
+ if ( ! empty( $existing_style ) && ! str_ends_with( $existing_style, ';' ) ) {
+ $updated_style = $existing_style . '; ';
+ }
+
+ $updated_style .= $styles['css'];
+ $tags->set_attribute( 'style', $updated_style );
+ }
+
+ return $tags->get_updated_html();
+ }
+
+ return $block_content;
+}
+
+// Register the block support.
+WP_Block_Supports::get_instance()->register(
+ 'background',
+ array(
+ 'register_attribute' => 'gutenberg_register_background_support',
+ )
+);
+
+add_filter( 'render_block', 'gutenberg_render_background_support', 10, 2 );
diff --git a/lib/block-supports/behaviors.php b/lib/block-supports/behaviors.php
index b6692195eb5fb..cf668ed22d887 100644
--- a/lib/block-supports/behaviors.php
+++ b/lib/block-supports/behaviors.php
@@ -2,6 +2,10 @@
/**
* Behaviors block support flag.
*
+ * This file will NOT be backported to Core. It exists to provide a
+ * migration path for theme.json files that used the deprecated "behaviors".
+ * This file will be removed from Gutenberg in version 17.0.0.
+ *
* @package gutenberg
*/
@@ -37,13 +41,23 @@ function gutenberg_register_behaviors_support( $block_type ) {
/**
* Add the directives and layout needed for the lightbox behavior.
- * This functions shouldn't be in this file. It should be moved to a package (or somewhere else), where all the behaviors logic is defined.
*
* @param string $block_content Rendered block content.
* @param array $block Block object.
* @return string Filtered block content.
*/
function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) {
+
+ // We've deprecated the lightbox implementation via behaviors.
+ // While we may continue to explore behaviors in the future, the lightbox
+ // logic seems very specific to the image and will likely never be a part
+ // of behaviors, even in the future. With that in mind, we've rewritten the lightbox
+ // to be a feature of the image block and will also soon remove the block_supports.
+ // *Note: This logic for generating the lightbox markup has been duplicated and moved
+ // to the image block's index.php.*
+ // See https://github.com/WordPress/gutenberg/issues/53403.
+ _deprecated_function( 'gutenberg_render_behaviors_support_lightbox', 'Gutenberg 17.0.0', '' );
+
$link_destination = isset( $block['attrs']['linkDestination'] ) ? $block['attrs']['linkDestination'] : 'none';
// Get the lightbox setting from the block attributes.
if ( isset( $block['attrs']['behaviors']['lightbox'] ) ) {
diff --git a/lib/block-supports/border.php b/lib/block-supports/border.php
index 486f6d99b6489..1a54371d082e1 100644
--- a/lib/block-supports/border.php
+++ b/lib/block-supports/border.php
@@ -94,14 +94,14 @@ function gutenberg_apply_border_support( $block_type, $block_attributes ) {
! wp_should_skip_block_supports_serialization( $block_type, '__experimentalBorder', 'color' )
) {
$preset_border_color = array_key_exists( 'borderColor', $block_attributes ) ? "var:preset|color|{$block_attributes['borderColor']}" : null;
- $custom_border_color = _wp_array_get( $block_attributes, array( 'style', 'border', 'color' ), null );
+ $custom_border_color = $block_attributes['style']['border']['color'] ?? null;
$border_block_styles['color'] = $preset_border_color ? $preset_border_color : $custom_border_color;
}
// Generate styles for individual border sides.
if ( $has_border_color_support || $has_border_width_support ) {
foreach ( array( 'top', 'right', 'bottom', 'left' ) as $side ) {
- $border = _wp_array_get( $block_attributes, array( 'style', 'border', $side ), null );
+ $border = $block_attributes['style']['border'][ $side ] ?? null;
$border_side_values = array(
'width' => isset( $border['width'] ) && ! wp_should_skip_block_supports_serialization( $block_type, '__experimentalBorder', 'width' ) ? $border['width'] : null,
'color' => isset( $border['color'] ) && ! wp_should_skip_block_supports_serialization( $block_type, '__experimentalBorder', 'color' ) ? $border['color'] : null,
@@ -134,24 +134,24 @@ function gutenberg_apply_border_support( $block_type, $block_attributes ) {
* flag nested under `experimentalBorder` must be enabled for the feature
* to be opted into.
*
- * @param WP_Block_Type $block_type Block type to check for support.
- * @param string $feature Name of the feature to check support for.
- * @param mixed $default Fallback value for feature support, defaults to false.
+ * @param WP_Block_Type $block_type Block type to check for support.
+ * @param string $feature Name of the feature to check support for.
+ * @param mixed $default_value Fallback value for feature support, defaults to false.
*
* @return boolean Whether or not the feature is supported.
*/
-function gutenberg_has_border_feature_support( $block_type, $feature, $default = false ) {
+function gutenberg_has_border_feature_support( $block_type, $feature, $default_value = false ) {
// Check if all border support features have been opted into via `"__experimentalBorder": true`.
- if (
- property_exists( $block_type, 'supports' ) &&
- ( true === _wp_array_get( $block_type->supports, array( '__experimentalBorder' ), $default ) )
- ) {
- return true;
+ if ( property_exists( $block_type, 'supports' ) ) {
+ $block_type_supports_border = $block_type->supports['__experimentalBorder'] ?? $default_value;
+ if ( true === $block_type_supports_border ) {
+ return true;
+ }
}
// Check if the specific feature has been opted into individually
// via nested flag under `__experimentalBorder`.
- return block_has_support( $block_type, array( '__experimentalBorder', $feature ), $default );
+ return block_has_support( $block_type, array( '__experimentalBorder', $feature ), $default_value );
}
// Register the block support.
diff --git a/lib/block-supports/colors.php b/lib/block-supports/colors.php
index 7ec32bf14d753..6919f58c067e4 100644
--- a/lib/block-supports/colors.php
+++ b/lib/block-supports/colors.php
@@ -11,15 +11,26 @@
* @param WP_Block_Type $block_type Block Type.
*/
function gutenberg_register_colors_support( $block_type ) {
- $color_support = property_exists( $block_type, 'supports' ) ? _wp_array_get( $block_type->supports, array( 'color' ), false ) : false;
- $has_text_colors_support = true === $color_support || ( is_array( $color_support ) && _wp_array_get( $color_support, array( 'text' ), true ) );
- $has_background_colors_support = true === $color_support || ( is_array( $color_support ) && _wp_array_get( $color_support, array( 'background' ), true ) );
- $has_gradients_support = _wp_array_get( $color_support, array( 'gradients' ), false );
- $has_link_colors_support = _wp_array_get( $color_support, array( 'link' ), false );
+ $color_support = false;
+ if ( property_exists( $block_type, 'supports' ) ) {
+ $color_support = $block_type->supports['color'] ?? false;
+ }
+ $has_text_colors_support = true === $color_support ||
+ ( isset( $color_support['text'] ) && $color_support['text'] ) ||
+ ( is_array( $color_support ) && ! isset( $color_support['text'] ) );
+ $has_background_colors_support = true === $color_support ||
+ ( isset( $color_support['background'] ) && $color_support['background'] ) ||
+ ( is_array( $color_support ) && ! isset( $color_support['background'] ) );
+ $has_gradients_support = $color_support['gradients'] ?? false;
+ $has_link_colors_support = $color_support['link'] ?? false;
+ $has_button_colors_support = $color_support['button'] ?? false;
+ $has_heading_colors_support = $color_support['heading'] ?? false;
$has_color_support = $has_text_colors_support ||
$has_background_colors_support ||
$has_gradients_support ||
- $has_link_colors_support;
+ $has_link_colors_support ||
+ $has_button_colors_support ||
+ $has_heading_colors_support;
if ( ! $block_type->attributes ) {
$block_type->attributes = array();
@@ -61,7 +72,7 @@ function gutenberg_register_colors_support( $block_type ) {
* @return array Colors CSS classes and inline styles.
*/
function gutenberg_apply_colors_support( $block_type, $block_attributes ) {
- $color_support = _wp_array_get( $block_type->supports, array( 'color' ), false );
+ $color_support = $block_type->supports['color'] ?? false;
if (
is_array( $color_support ) &&
@@ -70,23 +81,27 @@ function gutenberg_apply_colors_support( $block_type, $block_attributes ) {
return array();
}
- $has_text_colors_support = true === $color_support || ( is_array( $color_support ) && _wp_array_get( $color_support, array( 'text' ), true ) );
- $has_background_colors_support = true === $color_support || ( is_array( $color_support ) && _wp_array_get( $color_support, array( 'background' ), true ) );
- $has_gradients_support = _wp_array_get( $color_support, array( 'gradients' ), false );
+ $has_text_colors_support = true === $color_support ||
+ ( isset( $color_support['text'] ) && $color_support['text'] ) ||
+ ( is_array( $color_support ) && ! isset( $color_support['text'] ) );
+ $has_background_colors_support = true === $color_support ||
+ ( isset( $color_support['background'] ) && $color_support['background'] ) ||
+ ( is_array( $color_support ) && ! isset( $color_support['background'] ) );
+ $has_gradients_support = $color_support['gradients'] ?? false;
$color_block_styles = array();
// Text colors.
// Check support for text colors.
if ( $has_text_colors_support && ! wp_should_skip_block_supports_serialization( $block_type, 'color', 'text' ) ) {
$preset_text_color = array_key_exists( 'textColor', $block_attributes ) ? "var:preset|color|{$block_attributes['textColor']}" : null;
- $custom_text_color = _wp_array_get( $block_attributes, array( 'style', 'color', 'text' ), null );
+ $custom_text_color = $block_attributes['style']['color']['text'] ?? null;
$color_block_styles['text'] = $preset_text_color ? $preset_text_color : $custom_text_color;
}
// Background colors.
if ( $has_background_colors_support && ! wp_should_skip_block_supports_serialization( $block_type, 'color', 'background' ) ) {
$preset_background_color = array_key_exists( 'backgroundColor', $block_attributes ) ? "var:preset|color|{$block_attributes['backgroundColor']}" : null;
- $custom_background_color = _wp_array_get( $block_attributes, array( 'style', 'color', 'background' ), null );
+ $custom_background_color = $block_attributes['style']['color']['background'] ?? null;
$color_block_styles['background'] = $preset_background_color ? $preset_background_color : $custom_background_color;
}
@@ -94,7 +109,7 @@ function gutenberg_apply_colors_support( $block_type, $block_attributes ) {
if ( $has_gradients_support && ! wp_should_skip_block_supports_serialization( $block_type, 'color', 'gradients' ) ) {
$preset_gradient_color = array_key_exists( 'gradient', $block_attributes ) ? "var:preset|gradient|{$block_attributes['gradient']}" : null;
- $custom_gradient_color = _wp_array_get( $block_attributes, array( 'style', 'color', 'gradient' ), null );
+ $custom_gradient_color = $block_attributes['style']['color']['gradient'] ?? null;
$color_block_styles['gradient'] = $preset_gradient_color ? $preset_gradient_color : $custom_gradient_color;
}
diff --git a/lib/block-supports/dimensions.php b/lib/block-supports/dimensions.php
index d4d15cd609e63..1ef43133c2cdf 100644
--- a/lib/block-supports/dimensions.php
+++ b/lib/block-supports/dimensions.php
@@ -61,8 +61,11 @@ function gutenberg_apply_dimensions_support( $block_type, $block_attributes ) {
$skip_min_height = wp_should_skip_block_supports_serialization( $block_type, 'dimensions', 'minHeight' );
$dimensions_block_styles = array();
- $dimensions_block_styles['minHeight'] = $has_min_height_support && ! $skip_min_height ? _wp_array_get( $block_styles, array( 'dimensions', 'minHeight' ), null ) : null;
- $styles = gutenberg_style_engine_get_styles( array( 'dimensions' => $dimensions_block_styles ) );
+ $dimensions_block_styles['minHeight'] = null;
+ if ( $has_min_height_support && ! $skip_min_height ) {
+ $dimensions_block_styles['minHeight'] = $block_styles['dimensions']['minHeight'] ?? null;
+ }
+ $styles = gutenberg_style_engine_get_styles( array( 'dimensions' => $dimensions_block_styles ) );
if ( ! empty( $styles['css'] ) ) {
$attributes['style'] = $styles['css'];
diff --git a/lib/block-supports/elements.php b/lib/block-supports/elements.php
index d4502aaa2e478..2106360c4bd04 100644
--- a/lib/block-supports/elements.php
+++ b/lib/block-supports/elements.php
@@ -5,16 +5,6 @@
* @package gutenberg
*/
-/**
- * Get the elements class names.
- *
- * @param array $block Block object.
- * @return string The unique class name.
- */
-function gutenberg_get_elements_class_name( $block ) {
- return 'wp-elements-' . md5( serialize( $block ) );
-}
-
/**
* Update the block content with elements class names.
*
@@ -23,34 +13,79 @@ function gutenberg_get_elements_class_name( $block ) {
* @return string Filtered block content.
*/
function gutenberg_render_elements_support( $block_content, $block ) {
- if ( ! $block_content ) {
+ if ( ! $block_content || empty( $block['attrs'] ) ) {
return $block_content;
}
- $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
- $skip_link_color_serialization = wp_should_skip_block_supports_serialization( $block_type, 'color', 'link' );
+ $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
+
+ $element_color_properties = array(
+ 'button' => array(
+ 'skip' => wp_should_skip_block_supports_serialization( $block_type, 'color', 'button' ),
+ 'paths' => array(
+ 'style.elements.button.color.text',
+ 'style.elements.button.color.background',
+ 'style.elements.button.color.gradient',
+ ),
+ ),
+ 'link' => array(
+ 'skip' => wp_should_skip_block_supports_serialization( $block_type, 'color', 'link' ),
+ 'paths' => array(
+ 'style.elements.link.color.text',
+ 'style.elements.link.:hover.color.text',
+ ),
+ ),
+ 'heading' => array(
+ 'skip' => wp_should_skip_block_supports_serialization( $block_type, 'color', 'heading' ),
+ 'paths' => array(
+ 'style.elements.heading.color.text',
+ 'style.elements.heading.color.background',
+ 'style.elements.heading.color.gradient',
+ 'style.elements.h1.color.text',
+ 'style.elements.h1.color.background',
+ 'style.elements.h1.color.gradient',
+ 'style.elements.h2.color.text',
+ 'style.elements.h2.color.background',
+ 'style.elements.h2.color.gradient',
+ 'style.elements.h3.color.text',
+ 'style.elements.h3.color.background',
+ 'style.elements.h3.color.gradient',
+ 'style.elements.h4.color.text',
+ 'style.elements.h4.color.background',
+ 'style.elements.h4.color.gradient',
+ 'style.elements.h5.color.text',
+ 'style.elements.h5.color.background',
+ 'style.elements.h5.color.gradient',
+ 'style.elements.h6.color.text',
+ 'style.elements.h6.color.background',
+ 'style.elements.h6.color.gradient',
+ ),
+ ),
+ );
+
+ $skip_all_element_color_serialization = $element_color_properties['button']['skip'] &&
+ $element_color_properties['link']['skip'] &&
+ $element_color_properties['heading']['skip'];
- if ( $skip_link_color_serialization ) {
+ if ( $skip_all_element_color_serialization ) {
return $block_content;
}
- $link_color = null;
- if ( ! empty( $block['attrs'] ) ) {
- $link_color = _wp_array_get( $block['attrs'], array( 'style', 'elements', 'link', 'color', 'text' ), null );
- }
+ $element_colors_set = 0;
+
+ foreach ( $element_color_properties as $element_config ) {
+ if ( $element_config['skip'] ) {
+ continue;
+ }
- $hover_link_color = null;
- if ( ! empty( $block['attrs'] ) ) {
- $hover_link_color = _wp_array_get( $block['attrs'], array( 'style', 'elements', 'link', ':hover', 'color', 'text' ), null );
+ foreach ( $element_config['paths'] as $path ) {
+ if ( null !== _wp_array_get( $block['attrs'], explode( '.', $path ), null ) ) {
+ ++$element_colors_set;
+ }
+ }
}
- /*
- * For now we only care about link colors.
- * This code in the future when we have a public API
- * should take advantage of WP_Theme_JSON_Gutenberg::compute_style_properties
- * and work for any element and style.
- */
- if ( null === $link_color && null === $hover_link_color ) {
+ if ( ! $element_colors_set ) {
return $block_content;
}
@@ -58,7 +93,7 @@ function gutenberg_render_elements_support( $block_content, $block ) {
// Add the class name to the first element, presuming it's the wrapper, if it exists.
$tags = new WP_HTML_Tag_Processor( $block_content );
if ( $tags->next_tag() ) {
- $tags->add_class( gutenberg_get_elements_class_name( $block ) );
+ $tags->add_class( wp_get_elements_class_name( $block ) );
}
return $tags->get_updated_html();
@@ -80,33 +115,84 @@ function gutenberg_render_elements_support_styles( $pre_render, $block ) {
$block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
$element_block_styles = isset( $block['attrs']['style']['elements'] ) ? $block['attrs']['style']['elements'] : null;
- /*
- * For now we only care about link color.
- */
- $skip_link_color_serialization = wp_should_skip_block_supports_serialization( $block_type, 'color', 'link' );
+ if ( ! $element_block_styles ) {
+ return null;
+ }
+
+ $skip_link_color_serialization = wp_should_skip_block_supports_serialization( $block_type, 'color', 'link' );
+ $skip_heading_color_serialization = wp_should_skip_block_supports_serialization( $block_type, 'color', 'heading' );
+ $skip_button_color_serialization = wp_should_skip_block_supports_serialization( $block_type, 'color', 'button' );
+ $skips_all_element_color_serialization = $skip_link_color_serialization &&
+ $skip_heading_color_serialization &&
+ $skip_button_color_serialization;
- if ( $skip_link_color_serialization ) {
+ if ( $skips_all_element_color_serialization ) {
return null;
}
- $class_name = gutenberg_get_elements_class_name( $block );
- $link_block_styles = isset( $element_block_styles['link'] ) ? $element_block_styles['link'] : null;
-
- gutenberg_style_engine_get_styles(
- $link_block_styles,
- array(
- 'selector' => ".$class_name a",
- 'context' => 'block-supports',
- )
+
+ $class_name = wp_get_elements_class_name( $block );
+
+ $element_types = array(
+ 'button' => array(
+ 'selector' => ".$class_name .wp-element-button, .$class_name .wp-block-button__link",
+ 'skip' => $skip_button_color_serialization,
+ ),
+ 'link' => array(
+ 'selector' => ".$class_name a",
+ 'hover_selector' => ".$class_name a:hover",
+ 'skip' => $skip_link_color_serialization,
+ ),
+ 'heading' => array(
+ 'selector' => ".$class_name h1, .$class_name h2, .$class_name h3, .$class_name h4, .$class_name h5, .$class_name h6",
+ 'skip' => $skip_heading_color_serialization,
+ 'elements' => array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ),
+ ),
);
- if ( isset( $link_block_styles[':hover'] ) ) {
- gutenberg_style_engine_get_styles(
- $link_block_styles[':hover'],
- array(
- 'selector' => ".$class_name a:hover",
- 'context' => 'block-supports',
- )
- );
+ foreach ( $element_types as $element_type => $element_config ) {
+ if ( $element_config['skip'] ) {
+ continue;
+ }
+
+ $element_style_object = _wp_array_get( $element_block_styles, array( $element_type ), null );
+
+ // Process primary element type styles.
+ if ( $element_style_object ) {
+ gutenberg_style_engine_get_styles(
+ $element_style_object,
+ array(
+ 'selector' => $element_config['selector'],
+ 'context' => 'block-supports',
+ )
+ );
+
+ if ( isset( $element_style_object[':hover'] ) ) {
+ gutenberg_style_engine_get_styles(
+ $element_style_object[':hover'],
+ array(
+ 'selector' => $element_config['hover_selector'],
+ 'context' => 'block-supports',
+ )
+ );
+ }
+ }
+
+ // Process related elements e.g. h1-h6 for headings.
+ if ( isset( $element_config['elements'] ) ) {
+ foreach ( $element_config['elements'] as $element ) {
+ $element_style_object = _wp_array_get( $element_block_styles, array( $element ), null );
+
+ if ( $element_style_object ) {
+ gutenberg_style_engine_get_styles(
+ $element_style_object,
+ array(
+ 'selector' => ".$class_name $element",
+ 'context' => 'block-supports',
+ )
+ );
+ }
+ }
+ }
}
return null;
diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php
index 6eecccfdc72c5..0fe217cf83270 100644
--- a/lib/block-supports/layout.php
+++ b/lib/block-supports/layout.php
@@ -222,7 +222,7 @@ function gutenberg_register_layout_support( $block_type ) {
* @return string CSS styles on success. Else, empty string.
*/
function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support = false, $gap_value = null, $should_skip_gap_serialization = false, $fallback_gap_value = '0.5em', $block_spacing = null ) {
- $layout_type = isset( $layout['type'] ) ? $layout['type'] : 'default';
+ $layout_type = $layout['type'] ?? 'default';
$layout_styles = array();
if ( 'default' === $layout_type ) {
@@ -401,7 +401,10 @@ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support
$gap_sides = is_array( $gap_value ) ? array( 'top', 'left' ) : array( 'top' );
foreach ( $gap_sides as $gap_side ) {
- $process_value = is_string( $gap_value ) ? $gap_value : _wp_array_get( $gap_value, array( $gap_side ), $fallback_gap_value );
+ $process_value = $gap_value;
+ if ( is_array( $gap_value ) ) {
+ $process_value = $gap_value[ $gap_side ] ?? $fallback_gap_value;
+ }
// Get spacing CSS variable from preset value if provided.
if ( is_string( $process_value ) && str_contains( $process_value, 'var:preset|spacing|' ) ) {
$index_to_splice = strrpos( $process_value, '|' ) + 1;
@@ -482,7 +485,10 @@ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support
$gap_sides = is_array( $gap_value ) ? array( 'top', 'left' ) : array( 'top' );
foreach ( $gap_sides as $gap_side ) {
- $process_value = is_string( $gap_value ) ? $gap_value : _wp_array_get( $gap_value, array( $gap_side ), $fallback_gap_value );
+ $process_value = $gap_value;
+ if ( is_array( $gap_value ) ) {
+ $process_value = $gap_value[ $gap_side ] ?? $fallback_gap_value;
+ }
// Get spacing CSS variable from preset value if provided.
if ( is_string( $process_value ) && str_contains( $process_value, 'var:preset|spacing|' ) ) {
$index_to_splice = strrpos( $process_value, '|' ) + 1;
@@ -529,24 +535,22 @@ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support
* @return string Filtered block content.
*/
function gutenberg_render_layout_support_flag( $block_content, $block ) {
- $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
- $support_layout = block_has_support( $block_type, array( 'layout' ), false ) || block_has_support( $block_type, array( '__experimentalLayout' ), false );
- $has_child_layout = isset( $block['attrs']['style']['layout']['selfStretch'] );
+ $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
+ $block_supports_layout = block_has_support( $block_type, array( 'layout' ), false ) || block_has_support( $block_type, array( '__experimentalLayout' ), false );
+ $layout_from_parent = $block['attrs']['style']['layout']['selfStretch'] ?? null;
- if ( ! $support_layout
- && ! $has_child_layout ) {
+ if ( ! $block_supports_layout && ! $layout_from_parent ) {
return $block_content;
}
$outer_class_names = array();
- if ( $has_child_layout && ( 'fixed' === $block['attrs']['style']['layout']['selfStretch'] || 'fill' === $block['attrs']['style']['layout']['selfStretch'] ) ) {
-
+ if ( 'fixed' === $layout_from_parent || 'fill' === $layout_from_parent ) {
$container_content_class = wp_unique_id( 'wp-container-content-' );
$child_layout_styles = array();
- if ( 'fixed' === $block['attrs']['style']['layout']['selfStretch'] && isset( $block['attrs']['style']['layout']['flexSize'] ) ) {
+ if ( 'fixed' === $layout_from_parent && isset( $block['attrs']['style']['layout']['flexSize'] ) ) {
$child_layout_styles[] = array(
'selector' => ".$container_content_class",
'declarations' => array(
@@ -554,7 +558,7 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) {
'box-sizing' => 'border-box',
),
);
- } elseif ( 'fill' === $block['attrs']['style']['layout']['selfStretch'] ) {
+ } elseif ( 'fill' === $layout_from_parent ) {
$child_layout_styles[] = array(
'selector' => ".$container_content_class",
'declarations' => array(
@@ -572,38 +576,48 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) {
);
$outer_class_names[] = $container_content_class;
+ }
+
+ // Prep the processor for modifying the block output.
+ $processor = new WP_HTML_Tag_Processor( $block_content );
+ // Having no tags implies there are no tags onto which to add class names.
+ if ( ! $processor->next_tag() ) {
+ return $block_content;
}
- // Return early if only child layout exists.
- if ( ! $support_layout && ! empty( $outer_class_names ) ) {
- $content = new WP_HTML_Tag_Processor( $block_content );
- $content->next_tag();
- $content->add_class( implode( ' ', $outer_class_names ) );
- return (string) $content;
+ /*
+ * A block may not support layout but still be affected by a parent block's layout.
+ *
+ * In these cases add the appropriate class names and then return early; there's
+ * no need to investigate on this block whether additional layout constraints apply.
+ */
+ if ( ! $block_supports_layout && ! empty( $outer_class_names ) ) {
+ foreach ( $outer_class_names as $class_name ) {
+ $processor->add_class( $class_name );
+ }
+ return $processor->get_updated_html();
}
$global_settings = gutenberg_get_global_settings();
- $fallback_layout = ! empty( _wp_array_get( $block_type->supports, array( 'layout', 'default' ), array() ) ) ? _wp_array_get( $block_type->supports, array( 'layout', 'default' ), array() ) : _wp_array_get( $block_type->supports, array( '__experimentalLayout', 'default' ), array() );
- $used_layout = isset( $block['attrs']['layout'] ) ? $block['attrs']['layout'] : $fallback_layout;
+ $fallback_layout = $block_type->supports['layout']['default'] ?? array();
+ if ( empty( $fallback_layout ) ) {
+ $fallback_layout = $block_type->supports['__experimentalLayout']['default'] ?? array();
+ }
+ $used_layout = $block['attrs']['layout'] ?? $fallback_layout;
$class_names = array();
$layout_definitions = gutenberg_get_layout_definitions();
$container_class = wp_unique_id( 'wp-container-' );
- $layout_classname = '';
// Set the correct layout type for blocks using legacy content width.
if ( isset( $used_layout['inherit'] ) && $used_layout['inherit'] || isset( $used_layout['contentSize'] ) && $used_layout['contentSize'] ) {
$used_layout['type'] = 'constrained';
}
- $root_padding_aware_alignments = _wp_array_get( $global_settings, array( 'useRootPaddingAwareAlignments' ), false );
+ $root_padding_aware_alignments = $global_settings['useRootPaddingAwareAlignments'] ?? false;
- if (
- $root_padding_aware_alignments &&
- isset( $used_layout['type'] ) &&
- 'constrained' === $used_layout['type']
- ) {
+ if ( $root_padding_aware_alignments && isset( $used_layout['type'] ) && 'constrained' === $used_layout['type'] ) {
$class_names[] = 'has-global-padding';
}
@@ -627,9 +641,9 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) {
// Get classname for layout type.
if ( isset( $used_layout['type'] ) ) {
- $layout_classname = _wp_array_get( $layout_definitions, array( $used_layout['type'], 'className' ), '' );
+ $layout_classname = $layout_definitions[ $used_layout['type'] ]['className'] ?? '';
} else {
- $layout_classname = _wp_array_get( $layout_definitions, array( 'default', 'className' ), '' );
+ $layout_classname = $layout_definitions['default']['className'] ?? '';
}
if ( $layout_classname && is_string( $layout_classname ) ) {
@@ -642,7 +656,7 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) {
*/
if ( ! current_theme_supports( 'disable-layout-styles' ) ) {
- $gap_value = _wp_array_get( $block, array( 'attrs', 'style', 'spacing', 'blockGap' ) );
+ $gap_value = $block['attrs']['style']['spacing']['blockGap'] ?? null;
/*
* Skip if gap value contains unsupported characters.
@@ -657,8 +671,8 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) {
$gap_value = $gap_value && preg_match( '%[\\\(&=}]|/\*%', $gap_value ) ? null : $gap_value;
}
- $fallback_gap_value = _wp_array_get( $block_type->supports, array( 'spacing', 'blockGap', '__experimentalDefault' ), '0.5em' );
- $block_spacing = _wp_array_get( $block, array( 'attrs', 'style', 'spacing' ), null );
+ $fallback_gap_value = $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ?? '0.5em';
+ $block_spacing = $block['attrs']['style']['spacing'] ?? null;
/*
* If a block's block.json skips serialization for spacing or spacing.blockGap,
@@ -666,7 +680,7 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) {
*/
$should_skip_gap_serialization = wp_should_skip_block_supports_serialization( $block_type, 'spacing', 'blockGap' );
- $block_gap = _wp_array_get( $global_settings, array( 'spacing', 'blockGap' ), null );
+ $block_gap = $global_settings['spacing']['blockGap'] ?? null;
$has_block_gap_support = isset( $block_gap );
$style = gutenberg_get_layout_style(
@@ -690,49 +704,100 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) {
$full_block_name = 'core' === $split_block_name[0] ? end( $split_block_name ) : implode( '-', $split_block_name );
$class_names[] = 'wp-block-' . $full_block_name . '-' . $layout_classname;
- $content_with_outer_classnames = '';
-
+ // Add classes to the outermost HTML tag if necessary.
if ( ! empty( $outer_class_names ) ) {
- $content_with_outer_classnames = new WP_HTML_Tag_Processor( $block_content );
- $content_with_outer_classnames->next_tag();
foreach ( $outer_class_names as $outer_class_name ) {
- $content_with_outer_classnames->add_class( $outer_class_name );
+ $processor->add_class( $outer_class_name );
}
-
- $content_with_outer_classnames = (string) $content_with_outer_classnames;
}
/**
- * The first chunk of innerContent contains the block markup up until the inner blocks start.
- * We want to target the opening tag of the inner blocks wrapper, which is the last tag in that chunk.
- */
- $inner_content_classnames = '';
-
- if ( isset( $block['innerContent'][0] ) && 'string' === gettype( $block['innerContent'][0] ) && count( $block['innerContent'] ) > 1 ) {
- $tags = new WP_HTML_Tag_Processor( $block['innerContent'][0] );
- $last_classnames = '';
- while ( $tags->next_tag() ) {
- $last_classnames = $tags->get_attribute( 'class' );
+ * Attempts to refer to the inner-block wrapping element by its class attribute.
+ *
+ * When examining a block's inner content, if a block has inner blocks, then
+ * the first content item will likely be a text (HTML) chunk immediately
+ * preceding the inner blocks. The last HTML tag in that chunk would then be
+ * an opening tag for an element that wraps the inner blocks.
+ *
+ * There's no reliable way to associate this wrapper in $block_content because
+ * it may have changed during the rendering pipeline (as inner contents is
+ * provided before rendering) and through previous filters. In many cases,
+ * however, the `class` attribute will be a good-enough identifier, so this
+ * code finds the last tag in that chunk and stores the `class` attribute
+ * so that it can be used later when working through the rendered block output
+ * to identify the wrapping element and add the remaining class names to it.
+ *
+ * It's also possible that no inner block wrapper even exists. If that's the
+ * case this code could apply the class names to an invalid element.
+ *
+ * Example:
+ *
+ * $block['innerBlocks'] = array( $list_item );
+ * $block['innerContent'] = array( '
', null, '
' );
+ *
+ * // After rendering, the initial contents may have been modified by other renderers or filters.
+ * $block_content = <<
+ *
+ *
Code
+ *
It's a list!
+ *
+ * HTML;
+ *
+ * Although it is possible that the original block-wrapper classes are changed in $block_content
+ * from how they appear in $block['innerContent'], it's likely that the original class attributes
+ * are still present in the wrapper as they are in this example. Frequently, additional classes
+ * will also be present; rarely should classes be removed.
+ *
+ * @TODO: Find a better way to match the first inner block. If it's possible to identify where the
+ * first inner block starts, then it will be possible to find the last tag before it starts
+ * and then that tag, if an opening tag, can be solidly identified as a wrapping element.
+ * Can some unique value or class or ID be added to the inner blocks when they process
+ * so that they can be extracted here safely without guessing? Can the block rendering function
+ * return information about where the rendered inner blocks start?
+ *
+ * @var string|null
+ */
+ $inner_block_wrapper_classes = null;
+ $first_chunk = $block['innerContent'][0] ?? null;
+ if ( is_string( $first_chunk ) && count( $block['innerContent'] ) > 1 ) {
+ $first_chunk_processor = new WP_HTML_Tag_Processor( $first_chunk );
+ while ( $first_chunk_processor->next_tag() ) {
+ $class_attribute = $first_chunk_processor->get_attribute( 'class' );
+ if ( is_string( $class_attribute ) && ! empty( $class_attribute ) ) {
+ $inner_block_wrapper_classes = $class_attribute;
+ }
}
-
- $inner_content_classnames = (string) $last_classnames;
}
- $content = $content_with_outer_classnames ? new WP_HTML_Tag_Processor( $content_with_outer_classnames ) : new WP_HTML_Tag_Processor( $block_content );
-
- if ( $inner_content_classnames ) {
- $content->next_tag( array( 'class_name' => $inner_content_classnames ) );
- foreach ( $class_names as $class_name ) {
- $content->add_class( $class_name );
+ /*
+ * If necessary, advance to what is likely to be an inner block wrapper tag.
+ *
+ * This advances until it finds the first tag containing the original class
+ * attribute from above. If none is found it will scan to the end of the block
+ * and fail to add any class names.
+ *
+ * If there is no block wrapper it won't advance at all, in which case the
+ * class names will be added to the first and outermost tag of the block.
+ * For cases where this outermost tag is the only tag surrounding inner
+ * blocks then the outer wrapper and inner wrapper are the same.
+ */
+ do {
+ if ( ! $inner_block_wrapper_classes ) {
+ break;
}
- } else {
- $content->next_tag();
- foreach ( $class_names as $class_name ) {
- $content->add_class( $class_name );
+
+ if ( false !== strpos( $processor->get_attribute( 'class' ), $inner_block_wrapper_classes ) ) {
+ break;
}
+ } while ( $processor->next_tag() );
+
+ // Add the remaining class names.
+ foreach ( $class_names as $class_name ) {
+ $processor->add_class( $class_name );
}
- return (string) $content;
+ return $processor->get_updated_html();
}
// Register the block support. (overrides core one).
@@ -777,7 +842,7 @@ function gutenberg_restore_group_inner_container( $block_content, $block ) {
);
$updated_content = preg_replace_callback(
$replace_regex,
- static function( $matches ) {
+ static function ( $matches ) {
return $matches[1] . '
' . $matches[2] . '
' . $matches[3];
},
$block_content
diff --git a/lib/block-supports/position.php b/lib/block-supports/position.php
index 62a51c486362e..930e726d12321 100644
--- a/lib/block-supports/position.php
+++ b/lib/block-supports/position.php
@@ -44,8 +44,8 @@ function gutenberg_render_position_support( $block_content, $block ) {
}
$global_settings = gutenberg_get_global_settings();
- $theme_has_sticky_support = _wp_array_get( $global_settings, array( 'position', 'sticky' ), false );
- $theme_has_fixed_support = _wp_array_get( $global_settings, array( 'position', 'fixed' ), false );
+ $theme_has_sticky_support = $global_settings['position']['sticky'] ?? false;
+ $theme_has_fixed_support = $global_settings['position']['fixed'] ?? false;
// Only allow output for position types that the theme supports.
$allowed_position_types = array();
@@ -56,11 +56,11 @@ function gutenberg_render_position_support( $block_content, $block ) {
$allowed_position_types[] = 'fixed';
}
- $style_attribute = _wp_array_get( $block, array( 'attrs', 'style' ), null );
+ $style_attribute = $block['attrs']['style'] ?? null;
$class_name = wp_unique_id( 'wp-container-' );
$selector = ".$class_name";
$position_styles = array();
- $position_type = _wp_array_get( $style_attribute, array( 'position', 'type' ), '' );
+ $position_type = $style_attribute['position']['type'] ?? '';
$wrapper_classes = array();
if (
@@ -71,7 +71,7 @@ function gutenberg_render_position_support( $block_content, $block ) {
$sides = array( 'top', 'right', 'bottom', 'left' );
foreach ( $sides as $side ) {
- $side_value = _wp_array_get( $style_attribute, array( 'position', $side ) );
+ $side_value = $style_attribute['position'][ $side ] ?? null;
if ( null !== $side_value ) {
/*
* For fixed or sticky top positions,
diff --git a/lib/block-supports/settings.php b/lib/block-supports/settings.php
index 7c0dd719f41c3..b175fe778ce1b 100644
--- a/lib/block-supports/settings.php
+++ b/lib/block-supports/settings.php
@@ -5,18 +5,6 @@
* @package gutenberg
*/
-/**
- * Get the class name used on block level presets.
- *
- * @access private
- *
- * @param array $block Block object.
- * @return string The unique class name.
- */
-function _gutenberg_get_presets_class_name( $block ) {
- return 'wp-settings-' . md5( serialize( $block ) );
-}
-
/**
* Update the block content with block level presets class name.
*
@@ -38,7 +26,7 @@ function _gutenberg_add_block_level_presets_class( $block_content, $block ) {
}
// return early if no settings are found on the block attributes.
- $block_settings = _wp_array_get( $block, array( 'attrs', 'settings' ), null );
+ $block_settings = $block['attrs']['settings'] ?? null;
if ( empty( $block_settings ) ) {
return $block_content;
}
@@ -47,7 +35,7 @@ function _gutenberg_add_block_level_presets_class( $block_content, $block ) {
// Add the class name to the first element, presuming it's the wrapper, if it exists.
$tags = new WP_HTML_Tag_Processor( $block_content );
if ( $tags->next_tag() ) {
- $tags->add_class( _gutenberg_get_presets_class_name( $block ) );
+ $tags->add_class( _wp_get_presets_class_name( $block ) );
}
return $tags->get_updated_html();
@@ -71,12 +59,12 @@ function _gutenberg_add_block_level_preset_styles( $pre_render, $block ) {
}
// return early if no settings are found on the block attributes.
- $block_settings = _wp_array_get( $block, array( 'attrs', 'settings' ), null );
+ $block_settings = $block['attrs']['settings'] ?? null;
if ( empty( $block_settings ) ) {
return null;
}
- $class_name = '.' . _gutenberg_get_presets_class_name( $block );
+ $class_name = '.' . _wp_get_presets_class_name( $block );
// the root selector for preset variables needs to target every possible block selector
// in order for the general setting to override any bock specific setting of a parent block or
@@ -129,7 +117,12 @@ function _gutenberg_add_block_level_preset_styles( $pre_render, $block ) {
);
if ( ! empty( $styles ) ) {
- gutenberg_enqueue_block_support_styles( $styles );
+ /*
+ * This method is deprecated since WordPress 6.2.
+ * We could enqueue these styles separately,
+ * or print them out with other settings presets.
+ */
+ wp_enqueue_block_support_styles( $styles );
}
return null;
diff --git a/lib/block-supports/spacing.php b/lib/block-supports/spacing.php
index e78fac6d62419..86e6c750185a5 100644
--- a/lib/block-supports/spacing.php
+++ b/lib/block-supports/spacing.php
@@ -52,12 +52,19 @@ function gutenberg_apply_spacing_support( $block_type, $block_attributes ) {
return $attributes;
}
- $skip_padding = wp_should_skip_block_supports_serialization( $block_type, 'spacing', 'padding' );
- $skip_margin = wp_should_skip_block_supports_serialization( $block_type, 'spacing', 'margin' );
- $spacing_block_styles = array();
- $spacing_block_styles['padding'] = $has_padding_support && ! $skip_padding ? _wp_array_get( $block_styles, array( 'spacing', 'padding' ), null ) : null;
- $spacing_block_styles['margin'] = $has_margin_support && ! $skip_margin ? _wp_array_get( $block_styles, array( 'spacing', 'margin' ), null ) : null;
- $styles = gutenberg_style_engine_get_styles( array( 'spacing' => $spacing_block_styles ) );
+ $skip_padding = wp_should_skip_block_supports_serialization( $block_type, 'spacing', 'padding' );
+ $skip_margin = wp_should_skip_block_supports_serialization( $block_type, 'spacing', 'margin' );
+ $spacing_block_styles = array(
+ 'padding' => null,
+ 'margin' => null,
+ );
+ if ( $has_padding_support && ! $skip_padding ) {
+ $spacing_block_styles['padding'] = $block_styles['spacing']['padding'] ?? null;
+ }
+ if ( $has_margin_support && ! $skip_margin ) {
+ $spacing_block_styles['margin'] = $block_styles['spacing']['margin'] ?? null;
+ }
+ $styles = gutenberg_style_engine_get_styles( array( 'spacing' => $spacing_block_styles ) );
if ( ! empty( $styles['css'] ) ) {
$attributes['style'] = $styles['css'];
diff --git a/lib/block-supports/typography.php b/lib/block-supports/typography.php
index c4e1316ac3893..5c051ee05cc2c 100644
--- a/lib/block-supports/typography.php
+++ b/lib/block-supports/typography.php
@@ -15,21 +15,21 @@ function gutenberg_register_typography_support( $block_type ) {
return;
}
- $typography_supports = _wp_array_get( $block_type->supports, array( 'typography' ), false );
+ $typography_supports = $block_type->supports['typography'] ?? false;
if ( ! $typography_supports ) {
return;
}
- $has_font_family_support = _wp_array_get( $typography_supports, array( '__experimentalFontFamily' ), false );
- $has_font_size_support = _wp_array_get( $typography_supports, array( 'fontSize' ), false );
- $has_font_style_support = _wp_array_get( $typography_supports, array( '__experimentalFontStyle' ), false );
- $has_font_weight_support = _wp_array_get( $typography_supports, array( '__experimentalFontWeight' ), false );
- $has_letter_spacing_support = _wp_array_get( $typography_supports, array( '__experimentalLetterSpacing' ), false );
- $has_line_height_support = _wp_array_get( $typography_supports, array( 'lineHeight' ), false );
- $has_text_columns_support = _wp_array_get( $typography_supports, array( 'textColumns' ), false );
- $has_text_decoration_support = _wp_array_get( $typography_supports, array( '__experimentalTextDecoration' ), false );
- $has_text_transform_support = _wp_array_get( $typography_supports, array( '__experimentalTextTransform' ), false );
- $has_writing_mode_support = _wp_array_get( $typography_supports, array( '__experimentalWritingMode' ), false );
+ $has_font_family_support = $typography_supports['__experimentalFontFamily'] ?? false;
+ $has_font_size_support = $typography_supports['fontSize'] ?? false;
+ $has_font_style_support = $typography_supports['__experimentalFontStyle'] ?? false;
+ $has_font_weight_support = $typography_supports['__experimentalFontWeight'] ?? false;
+ $has_letter_spacing_support = $typography_supports['__experimentalLetterSpacing'] ?? false;
+ $has_line_height_support = $typography_supports['lineHeight'] ?? false;
+ $has_text_columns_support = $typography_supports['textColumns'] ?? false;
+ $has_text_decoration_support = $typography_supports['__experimentalTextDecoration'] ?? false;
+ $has_text_transform_support = $typography_supports['__experimentalTextTransform'] ?? false;
+ $has_writing_mode_support = $typography_supports['__experimentalWritingMode'] ?? false;
$has_typography_support = $has_font_family_support
|| $has_font_size_support
@@ -80,7 +80,7 @@ function gutenberg_apply_typography_support( $block_type, $block_attributes ) {
return array();
}
- $typography_supports = _wp_array_get( $block_type->supports, array( 'typography' ), false );
+ $typography_supports = $block_type->supports['typography'] ?? false;
if ( ! $typography_supports ) {
return array();
}
@@ -89,16 +89,16 @@ function gutenberg_apply_typography_support( $block_type, $block_attributes ) {
return array();
}
- $has_font_family_support = _wp_array_get( $typography_supports, array( '__experimentalFontFamily' ), false );
- $has_font_size_support = _wp_array_get( $typography_supports, array( 'fontSize' ), false );
- $has_font_style_support = _wp_array_get( $typography_supports, array( '__experimentalFontStyle' ), false );
- $has_font_weight_support = _wp_array_get( $typography_supports, array( '__experimentalFontWeight' ), false );
- $has_letter_spacing_support = _wp_array_get( $typography_supports, array( '__experimentalLetterSpacing' ), false );
- $has_line_height_support = _wp_array_get( $typography_supports, array( 'lineHeight' ), false );
- $has_text_columns_support = _wp_array_get( $typography_supports, array( 'textColumns' ), false );
- $has_text_decoration_support = _wp_array_get( $typography_supports, array( '__experimentalTextDecoration' ), false );
- $has_text_transform_support = _wp_array_get( $typography_supports, array( '__experimentalTextTransform' ), false );
- $has_writing_mode_support = _wp_array_get( $typography_supports, array( '__experimentalWritingMode' ), false );
+ $has_font_family_support = $typography_supports['__experimentalFontFamily'] ?? false;
+ $has_font_size_support = $typography_supports['fontSize'] ?? false;
+ $has_font_style_support = $typography_supports['__experimentalFontStyle'] ?? false;
+ $has_font_weight_support = $typography_supports['__experimentalFontWeight'] ?? false;
+ $has_letter_spacing_support = $typography_supports['__experimentalLetterSpacing'] ?? false;
+ $has_line_height_support = $typography_supports['lineHeight'] ?? false;
+ $has_text_columns_support = $typography_supports['textColumns'] ?? false;
+ $has_text_decoration_support = $typography_supports['__experimentalTextDecoration'] ?? false;
+ $has_text_transform_support = $typography_supports['__experimentalTextTransform'] ?? false;
+ $has_writing_mode_support = $typography_supports['__experimentalWritingMode'] ?? false;
// Whether to skip individual block support features.
$should_skip_font_size = wp_should_skip_block_supports_serialization( $block_type, 'typography', 'fontSize' );
@@ -140,11 +140,11 @@ function gutenberg_apply_typography_support( $block_type, $block_attributes ) {
}
if ( $has_line_height_support && ! $should_skip_line_height ) {
- $typography_block_styles['lineHeight'] = _wp_array_get( $block_attributes, array( 'style', 'typography', 'lineHeight' ), null );
+ $typography_block_styles['lineHeight'] = $block_attributes['style']['typography']['lineHeight'] ?? null;
}
if ( $has_text_columns_support && ! $should_skip_text_columns && isset( $block_attributes['style']['typography']['textColumns'] ) ) {
- $typography_block_styles['textColumns'] = _wp_array_get( $block_attributes, array( 'style', 'typography', 'textColumns' ), null );
+ $typography_block_styles['textColumns'] = $block_attributes['style']['typography']['textColumns'] ?? null;
}
if ( $has_text_decoration_support && ! $should_skip_text_decoration && isset( $block_attributes['style']['typography']['textDecoration'] ) ) {
@@ -163,7 +163,7 @@ function gutenberg_apply_typography_support( $block_type, $block_attributes ) {
}
if ( $has_writing_mode_support && ! $should_skip_writing_mode && isset( $block_attributes['style']['typography']['writingMode'] ) ) {
- $typography_block_styles['writingMode'] = _wp_array_get( $block_attributes, array( 'style', 'typography', 'writingMode' ), null );
+ $typography_block_styles['writingMode'] = $block_attributes['style']['typography']['writingMode'] ?? null;
}
$attributes = array();
@@ -244,7 +244,6 @@ function gutenberg_render_typography_support( $block_content, $block ) {
}
return $block_content;
-
}
/**
@@ -415,7 +414,14 @@ function gutenberg_get_computed_fluid_typography_value( $args = array() ) {
/**
* Returns a font-size value based on a given font-size preset.
- * Takes into account fluid typography parameters and attempts to return a css formula depending on available, valid values.
+ * Takes into account fluid typography parameters and attempts to return a CSS
+ * formula depending on available, valid values.
+ *
+ * @since 6.1.0
+ * @since 6.1.1 Adjusted rules for min and max font sizes.
+ * @since 6.2.0 Added 'settings.typography.fluid.minFontSize' support.
+ * @since 6.3.0 Using layout.wideSize as max viewport width, and logarithmic scale factor to calculate minimum font scale.
+ * @since 6.4.0 Added configurable min and max viewport width values to the typography.fluid theme.json schema.
*
* @param array $preset {
* Required. fontSizes preset value as seen in theme.json.
diff --git a/lib/blocks.php b/lib/blocks.php
index f160d2a7080d3..537fa9ce4b45e 100644
--- a/lib/blocks.php
+++ b/lib/blocks.php
@@ -21,10 +21,8 @@ function gutenberg_reregister_core_block_types() {
'code',
'column',
'columns',
- 'comments',
'details',
'group',
- 'footnotes',
'html',
'list',
'list-item',
@@ -276,7 +274,7 @@ function gutenberg_register_core_block_assets( $block_name ) {
if ( ! $stylesheet_removed ) {
add_action(
'wp_enqueue_scripts',
- static function() {
+ static function () {
wp_dequeue_style( 'wp-block-library-theme' );
}
);
@@ -433,4 +431,33 @@ function gutenberg_legacy_wp_block_post_meta( $value, $object_id, $meta_key, $si
return $value;
}
+
add_filter( 'default_post_metadata', 'gutenberg_legacy_wp_block_post_meta', 10, 4 );
+
+/**
+ * Complements the lightbox implementation for the 'core/image' block.
+ *
+ * This function is INTENTIONALLY left out of core as it only provides
+ * backwards compatibility for the legacy lightbox syntax that was only
+ * introduced in Gutenberg. The legacy syntax was using the `behaviors` key in
+ * the block attrbutes and the `theme.json` file.
+ *
+ * @since 16.7.0
+ *
+ * @param array $block The block to check.
+ * @return array The block with the legacyLightboxSettings set if available.
+ */
+function gutenberg_should_render_lightbox( $block ) {
+
+ if ( 'core/image' !== $block['blockName'] ) {
+ return $block;
+ }
+
+ if ( isset( $block['attrs']['behaviors']['lightbox'] ) ) {
+ $block['legacyLightboxSettings'] = $block['attrs']['behaviors']['lightbox'];
+ }
+
+ return $block;
+}
+
+add_filter( 'render_block_data', 'gutenberg_should_render_lightbox', 15, 1 );
diff --git a/lib/class-wp-duotone-gutenberg.php b/lib/class-wp-duotone-gutenberg.php
index 41120a882ed23..270c13875787f 100644
--- a/lib/class-wp-duotone-gutenberg.php
+++ b/lib/class-wp-duotone-gutenberg.php
@@ -482,7 +482,7 @@ private static function is_preset( $duotone_attr ) {
* @return string The CSS variable name.
*/
private static function get_css_custom_property_name( $slug ) {
- return "--wp--preset--duotone--$slug";
+ return "--wp--preset--duotone--$slug";
}
/**
@@ -492,7 +492,7 @@ private static function get_css_custom_property_name( $slug ) {
* @return string The ID of the duotone filter.
*/
private static function get_filter_id( $slug ) {
- return "wp-duotone-$slug";
+ return "wp-duotone-$slug";
}
/**
@@ -655,14 +655,14 @@ private static function get_selector( $block_name ) {
// `supports.filter.duotone` has not been set and the experimental
// property has been, the experimental property value is copied into
// `supports.filter.duotone`.
- $duotone_support = _wp_array_get( $block_type->supports, array( 'filter', 'duotone' ), false );
+ $duotone_support = $block_type->supports['filter']['duotone'] ?? false;
if ( ! $duotone_support ) {
return null;
}
// If the experimental duotone support was set, that value is to be
// treated as a selector and requires scoping.
- $experimental_duotone = _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), false );
+ $experimental_duotone = $block_type->supports['color']['__experimentalDuotone'] ?? false;
if ( $experimental_duotone ) {
$root_selector = wp_get_block_css_selector( $block_type );
return is_string( $experimental_duotone )
@@ -750,7 +750,7 @@ public static function register_duotone_support( $block_type ) {
if ( property_exists( $block_type, 'supports' ) ) {
// Previous `color.__experimentalDuotone` support flag is migrated
// to `filter.duotone` via `block_type_metadata_settings` filter.
- $has_duotone_support = _wp_array_get( $block_type->supports, array( 'filter', 'duotone' ), null );
+ $has_duotone_support = $block_type->supports['filter']['duotone'] ?? null;
}
if ( $has_duotone_support ) {
@@ -775,7 +775,7 @@ public static function register_duotone_support( $block_type ) {
public static function set_global_styles_presets() {
// Get the per block settings from the theme.json.
$tree = gutenberg_get_global_settings();
- $presets_by_origin = _wp_array_get( $tree, array( 'color', 'duotone' ), array() );
+ $presets_by_origin = $tree['color']['duotone'] ?? array();
foreach ( $presets_by_origin as $presets ) {
foreach ( $presets as $preset ) {
@@ -995,7 +995,7 @@ public static function add_editor_settings( $settings ) {
* @return array Filtered block type settings.
*/
public static function migrate_experimental_duotone_support_flag( $settings, $metadata ) {
- $duotone_support = _wp_array_get( $metadata, array( 'supports', 'color', '__experimentalDuotone' ), null );
+ $duotone_support = $metadata['supports']['color']['__experimentalDuotone'] ?? null;
if ( ! isset( $settings['supports']['filter']['duotone'] ) && null !== $duotone_support ) {
_wp_array_set( $settings, array( 'supports', 'filter', 'duotone' ), (bool) $duotone_support );
diff --git a/lib/class-wp-rest-global-styles-controller-gutenberg.php b/lib/class-wp-rest-global-styles-controller-gutenberg.php
index 499a410b16f8e..8125b2ebf1c28 100644
--- a/lib/class-wp-rest-global-styles-controller-gutenberg.php
+++ b/lib/class-wp-rest-global-styles-controller-gutenberg.php
@@ -302,7 +302,6 @@ public function update_item( $request ) {
*
* @since 5.9.0
* @since 6.2.0 Added validation of styles.css property.
- * @since 6.4.0 Added validation of behaviors property.
*
* @param WP_REST_Request $request Request object.
* @return stdClass|WP_Error Prepared item on success. WP_Error on when the custom CSS is not valid.
@@ -322,7 +321,7 @@ protected function prepare_item_for_database( $request ) {
}
}
- if ( isset( $request['styles'] ) || isset( $request['settings'] ) || isset( $request['behaviors'] ) ) {
+ if ( isset( $request['styles'] ) || isset( $request['settings'] ) ) {
$config = array();
if ( isset( $request['styles'] ) ) {
if ( isset( $request['styles']['css'] ) ) {
@@ -340,11 +339,6 @@ protected function prepare_item_for_database( $request ) {
} elseif ( isset( $existing_config['settings'] ) ) {
$config['settings'] = $existing_config['settings'];
}
- if ( isset( $request['behaviors'] ) ) {
- $config['behaviors'] = $request['behaviors'];
- } elseif ( isset( $existing_config['behaviors'] ) ) {
- $config['behaviors'] = $existing_config['behaviors'];
- }
$config['isGlobalStylesUserThemeJSON'] = true;
$config['version'] = WP_Theme_JSON_Gutenberg::LATEST_SCHEMA;
$changes->post_content = wp_json_encode( $config );
@@ -367,7 +361,6 @@ protected function prepare_item_for_database( $request ) {
*
* @since 5.9.0
* @since 6.2.0 Handling of style.css was added to WP_Theme_JSON.
- * @since 6.4.0 Added `behavior` field.
*
* @param WP_Post $post Global Styles post object.
* @param WP_REST_Request $request Request object.
@@ -411,10 +404,6 @@ public function prepare_item_for_response( $post, $request ) { // phpcs:ignore V
$data['styles'] = ! empty( $config['styles'] ) && $is_global_styles_user_theme_json ? $config['styles'] : new stdClass();
}
- if ( rest_is_field_included( 'behaviors', $fields ) ) {
- $data['behaviors'] = ! empty( $config['behaviors'] ) && $is_global_styles_user_theme_json ? $config['behaviors'] : new stdClass();
- }
-
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
@@ -521,7 +510,6 @@ public function get_collection_params() {
* Retrieves the global styles type' schema, conforming to JSON Schema.
*
* @since 5.9.0
- * @since 6.4.0 Added `behaviors` property.
*
* @return array Item schema data.
*/
@@ -535,28 +523,23 @@ public function get_item_schema() {
'title' => $this->post_type,
'type' => 'object',
'properties' => array(
- 'id' => array(
+ 'id' => array(
'description' => __( 'ID of global styles config.', 'gutenberg' ),
'type' => 'string',
'context' => array( 'embed', 'view', 'edit' ),
'readonly' => true,
),
- 'styles' => array(
+ 'styles' => array(
'description' => __( 'Global styles.', 'gutenberg' ),
'type' => array( 'object' ),
'context' => array( 'view', 'edit' ),
),
- 'settings' => array(
+ 'settings' => array(
'description' => __( 'Global settings.', 'gutenberg' ),
'type' => array( 'object' ),
'context' => array( 'view', 'edit' ),
),
- 'behaviors' => array(
- 'description' => __( 'Global behaviors.', 'default' ),
- 'type' => array( 'object' ),
- 'context' => array( 'view', 'edit' ),
- ),
- 'title' => array(
+ 'title' => array(
'description' => __( 'Title of the global styles variation.', 'gutenberg' ),
'type' => array( 'object', 'string' ),
'default' => '',
@@ -614,7 +597,6 @@ public function get_theme_item_permissions_check( $request ) { // phpcs:ignore V
* Returns the given theme global styles config.
*
* @since 5.9.0
- * @since 6.4.0 Added value for `behaviors` rest field.
*
* @param WP_REST_Request $request The request instance.
* @return WP_REST_Response|WP_Error
@@ -642,11 +624,6 @@ public function get_theme_item( $request ) {
$data['styles'] = isset( $raw_data['styles'] ) ? $raw_data['styles'] : array();
}
- if ( rest_is_field_included( 'behaviors', $fields ) ) {
- $raw_data = $theme->get_raw_data();
- $data['behaviors'] = isset( $raw_data['behaviors'] ) ? $raw_data['behaviors'] : array();
- }
-
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
diff --git a/lib/class-wp-theme-json-data-gutenberg.php b/lib/class-wp-theme-json-data-gutenberg.php
index db0737ebea08b..f0d2025347493 100644
--- a/lib/class-wp-theme-json-data-gutenberg.php
+++ b/lib/class-wp-theme-json-data-gutenberg.php
@@ -72,5 +72,4 @@ public function update_with( $new_data ) {
public function get_data() {
return $this->theme_json->get_raw_data();
}
-
}
diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php
index 223fd4a7fd8b1..59a3bbc3eb2ef 100644
--- a/lib/class-wp-theme-json-gutenberg.php
+++ b/lib/class-wp-theme-json-gutenberg.php
@@ -331,7 +331,6 @@ class WP_Theme_JSON_Gutenberg {
'templateParts',
'title',
'version',
- 'behaviors',
);
/**
@@ -347,11 +346,15 @@ class WP_Theme_JSON_Gutenberg {
* `position.fixed` and `position.sticky`.
* @since 6.3.0 Removed `layout.definitions`. Added `typography.writingMode`.
* @since 6.4.0 Added `layout.allowEditing`.
+ * @since 6.4.0 Added `lightbox`.
* @var array
*/
const VALID_SETTINGS = array(
'appearanceTools' => null,
'useRootPaddingAwareAlignments' => null,
+ 'background' => array(
+ 'backgroundImage' => null,
+ ),
'border' => array(
'color' => null,
'radius' => null,
@@ -384,6 +387,10 @@ class WP_Theme_JSON_Gutenberg {
'wideSize' => null,
'allowEditing' => null,
),
+ 'lightbox' => array(
+ 'enabled' => null,
+ 'allowEditing' => null,
+ ),
'position' => array(
'fixed' => null,
'sticky' => null,
@@ -416,7 +423,6 @@ class WP_Theme_JSON_Gutenberg {
'textTransform' => null,
'writingMode' => null,
),
- 'behaviors' => null,
);
/**
@@ -569,6 +575,7 @@ public static function get_element_class_name( $element ) {
* @var array
*/
const APPEARANCE_TOOLS_OPT_INS = array(
+ array( 'background', 'backgroundImage' ),
array( 'border', 'color' ),
array( 'border', 'radius' ),
array( 'border', 'style' ),
@@ -614,7 +621,7 @@ public function __construct( $theme_json = array(), $origin = 'theme' ) {
$origin = 'theme';
}
- $this->theme_json = WP_Theme_JSON_Schema::migrate( $theme_json );
+ $this->theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json );
$registry = WP_Block_Type_Registry::get_instance();
$valid_block_names = array_keys( $registry->get_all_registered() );
$valid_element_names = array_keys( static::ELEMENTS );
@@ -921,7 +928,7 @@ protected static function get_blocks_metadata() {
// Keep backwards compatibility for support.color.__experimentalDuotone.
if ( null === $duotone_selector ) {
- $duotone_support = _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), null );
+ $duotone_support = $block_type->supports['color']['__experimentalDuotone'] ?? null;
if ( $duotone_support ) {
$root_selector = wp_get_block_css_selector( $block_type );
@@ -1154,12 +1161,12 @@ protected function process_blocks_custom_css( $css, $selector ) {
*/
public function get_custom_css() {
// Add the global styles root CSS.
- $stylesheet = _wp_array_get( $this->theme_json, array( 'styles', 'css' ), '' );
+ $stylesheet = $this->theme_json['styles']['css'] ?? '';
// Add the global styles block CSS.
if ( isset( $this->theme_json['styles']['blocks'] ) ) {
foreach ( $this->theme_json['styles']['blocks'] as $name => $node ) {
- $custom_block_css = _wp_array_get( $this->theme_json, array( 'styles', 'blocks', $name, 'css' ) );
+ $custom_block_css = $this->theme_json['styles']['blocks'][ $name ]['css'] ?? null;
if ( $custom_block_css ) {
$selector = static::$blocks_metadata[ $name ]['selector'];
$stylesheet .= $this->process_blocks_custom_css( $custom_block_css, $selector );
@@ -1277,7 +1284,7 @@ protected function get_layout_styles( $block_metadata ) {
}
$selector = isset( $block_metadata['selector'] ) ? $block_metadata['selector'] : '';
- $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null;
+ $has_block_gap_support = isset( $this->theme_json['settings']['spacing']['blockGap'] );
$has_fallback_gap_support = ! $has_block_gap_support; // This setting isn't useful yet: it exists as a placeholder for a future explicit fallback gap styles support.
$node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
$layout_definitions = gutenberg_get_layout_definitions();
@@ -1291,7 +1298,7 @@ protected function get_layout_styles( $block_metadata ) {
if ( ! $has_block_gap_support ) {
$block_gap_value = static::ROOT_BLOCK_SELECTOR === $selector ? '0.5em' : null;
if ( ! empty( $block_type ) ) {
- $block_gap_value = _wp_array_get( $block_type->supports, array( 'spacing', 'blockGap', '__experimentalDefault' ), null );
+ $block_gap_value = $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ?? null;
}
} else {
$block_gap_value = static::get_property_value( $node, array( 'spacing', 'blockGap' ) );
@@ -1317,8 +1324,8 @@ protected function get_layout_styles( $block_metadata ) {
continue;
}
- $class_name = _wp_array_get( $layout_definition, array( 'className' ), false );
- $spacing_rules = _wp_array_get( $layout_definition, array( 'spacingStyles' ), array() );
+ $class_name = $layout_definition['className'] ?? false;
+ $spacing_rules = $layout_definition['spacingStyles'] ?? array();
if (
! empty( $class_name ) &&
@@ -1374,8 +1381,8 @@ protected function get_layout_styles( $block_metadata ) {
) {
$valid_display_modes = array( 'block', 'flex', 'grid' );
foreach ( $layout_definitions as $layout_definition ) {
- $class_name = _wp_array_get( $layout_definition, array( 'className' ), false );
- $base_style_rules = _wp_array_get( $layout_definition, array( 'baseStyles' ), array() );
+ $class_name = $layout_definition['className'] ?? false;
+ $base_style_rules = $layout_definition['baseStyles'] ?? array();
if (
! empty( $class_name ) &&
@@ -1807,7 +1814,7 @@ protected static function compute_preset_vars( $settings, $origins ) {
*/
protected static function compute_theme_vars( $settings ) {
$declarations = array();
- $custom_values = _wp_array_get( $settings, array( 'custom' ), array() );
+ $custom_values = $settings['custom'] ?? array();
$css_vars = static::flatten_tree( $custom_values );
foreach ( $css_vars as $key => $value ) {
$declarations[] = array(
@@ -2319,7 +2326,7 @@ public function get_styles_for_block( $block_metadata ) {
$node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
$use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
$selector = $block_metadata['selector'];
- $settings = _wp_array_get( $this->theme_json, array( 'settings' ) );
+ $settings = $this->theme_json['settings'] ?? null;
$feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $node );
@@ -2341,7 +2348,7 @@ public function get_styles_for_block( $block_metadata ) {
// Prepend the variation selector to the current selector.
$split_selectors = explode( ',', $shortened_selector );
$updated_selectors = array_map(
- static function( $split_selector ) use ( $clean_style_variation_selector ) {
+ static function ( $split_selector ) use ( $clean_style_variation_selector ) {
return $clean_style_variation_selector . $split_selector;
},
$split_selectors
@@ -2380,7 +2387,7 @@ static function( $split_selector ) use ( $clean_style_variation_selector ) {
$pseudo_matches = array_values(
array_filter(
$element_pseudo_allowed,
- static function( $pseudo_selector ) use ( $selector ) {
+ static function ( $pseudo_selector ) use ( $selector ) {
return str_contains( $selector, $pseudo_selector );
}
)
@@ -2472,7 +2479,7 @@ static function( $pseudo_selector ) use ( $selector ) {
*/
public function get_root_layout_rules( $selector, $block_metadata ) {
$css = '';
- $settings = _wp_array_get( $this->theme_json, array( 'settings' ) );
+ $settings = $this->theme_json['settings'] ?? array();
$use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
/*
@@ -2506,11 +2513,11 @@ public function get_root_layout_rules( $selector, $block_metadata ) {
// Right and left padding are applied to the first container with `.has-global-padding` class.
$css .= '.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }';
// Nested containers with `.has-global-padding` class do not get padding.
- $css .= '.has-global-padding :where(.has-global-padding) { padding-right: 0; padding-left: 0; }';
+ $css .= '.has-global-padding :where(.has-global-padding:not(.wp-block-block)) { padding-right: 0; padding-left: 0; }';
// Alignfull children of the container with left and right padding have negative margins so they can still be full width.
$css .= '.has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }';
// The above rule is negated for alignfull children of nested containers.
- $css .= '.has-global-padding :where(.has-global-padding) > .alignfull { margin-right: 0; margin-left: 0; }';
+ $css .= '.has-global-padding :where(.has-global-padding:not(.wp-block-block)) > .alignfull { margin-right: 0; margin-left: 0; }';
// Some of the children of alignfull blocks without content width should also get padding: text blocks and non-alignfull container blocks.
$css .= '.has-global-padding > .alignfull:where(:not(.has-global-padding):not(.is-layout-flex):not(.is-layout-grid)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),.wp-block:not(.alignfull),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }';
// The above rule also has to be negated for blocks inside nested `.has-global-padding` blocks.
@@ -2521,8 +2528,8 @@ public function get_root_layout_rules( $selector, $block_metadata ) {
$css .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
$css .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';
- $block_gap_value = _wp_array_get( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ), '0.5em' );
- $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null;
+ $block_gap_value = $this->theme_json['styles']['spacing']['blockGap'] ?? '0.5em';
+ $has_block_gap_support = isset( $this->theme_json['settings']['spacing']['blockGap'] );
if ( $has_block_gap_support ) {
$block_gap_value = static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ) );
$css .= ":where(.wp-site-blocks) > * { margin-block-start: $block_gap_value; margin-block-end: 0; }";
@@ -2857,7 +2864,7 @@ protected static function filter_slugs( $node, $slugs ) {
public static function remove_insecure_properties( $theme_json ) {
$sanitized = array();
- $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json );
+ $theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json );
$valid_block_names = array_keys( static::get_blocks_metadata() );
$valid_element_names = array_keys( static::ELEMENTS );
@@ -3353,7 +3360,7 @@ public function get_data() {
* @return null|void
*/
public function set_spacing_sizes() {
- $spacing_scale = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'spacingScale' ), array() );
+ $spacing_scale = $this->theme_json['settings']['spacing']['spacingScale'] ?? array();
// Gutenberg didn't have the 1st isset check.
if ( ! isset( $spacing_scale['steps'] )
@@ -3411,7 +3418,7 @@ public function set_spacing_sizes() {
}
if ( $below_midpoint_count < $steps_mid_point - 2 ) {
- $x_small_count++;
+ ++$x_small_count;
}
$slug -= 10;
@@ -3448,7 +3455,7 @@ public function set_spacing_sizes() {
}
if ( $above_midpoint_count > 1 ) {
- $x_large_count++;
+ ++$x_large_count;
}
$slug += 10;
@@ -3539,7 +3546,7 @@ protected function get_feature_declarations_for_node( $metadata, &$node ) {
return $declarations;
}
- $settings = _wp_array_get( $this->theme_json, array( 'settings' ) );
+ $settings = $this->theme_json['settings'] ?? null;
foreach ( $metadata['selectors'] as $feature => $feature_selectors ) {
// Skip if this is the block's root selector or the block doesn't
@@ -3724,10 +3731,10 @@ public static function resolve_variables( $theme_json ) {
$theme_vars = static::compute_theme_vars( $settings );
$vars = array_reduce(
array_merge( $preset_vars, $theme_vars ),
- function( $carry, $item ) {
+ function ( $carry, $item ) {
$name = $item['name'];
$carry[ "var({$name})" ] = $item['value'];
- return $carry;
+ return $carry;
},
array()
);
@@ -3735,5 +3742,4 @@ function( $carry, $item ) {
$theme_json->theme_json['styles'] = self::convert_variables_to_value( $styles, $vars );
return $theme_json;
}
-
}
diff --git a/lib/class-wp-theme-json-resolver-gutenberg.php b/lib/class-wp-theme-json-resolver-gutenberg.php
index 39721742946cd..950d9e00e6243 100644
--- a/lib/class-wp-theme-json-resolver-gutenberg.php
+++ b/lib/class-wp-theme-json-resolver-gutenberg.php
@@ -241,9 +241,9 @@ public static function get_theme_data( $deprecated = array(), $options = array()
$options = wp_parse_args( $options, array( 'with_supports' => true ) );
if ( null === static::$theme || ! static::has_same_registered_blocks( 'theme' ) ) {
- $theme_json_file = static::get_file_path_from_theme( 'theme.json' );
$wp_theme = wp_get_theme();
- if ( '' !== $theme_json_file ) {
+ $theme_json_file = $wp_theme->get_file_path( 'theme.json' );
+ if ( is_readable( $theme_json_file ) ) {
$theme_json_data = static::read_json_file( $theme_json_file );
$theme_json_data = static::translate( $theme_json_data, $wp_theme->get( 'TextDomain' ) );
} else {
@@ -263,8 +263,8 @@ public static function get_theme_data( $deprecated = array(), $options = array()
if ( $wp_theme->parent() ) {
// Get parent theme.json.
- $parent_theme_json_file = static::get_file_path_from_theme( 'theme.json', true );
- if ( '' !== $parent_theme_json_file ) {
+ $parent_theme_json_file = $wp_theme->parent()->get_file_path( 'theme.json' );
+ if ( $theme_json_file !== $parent_theme_json_file && is_readable( $parent_theme_json_file ) ) {
$parent_theme_json_data = static::read_json_file( $parent_theme_json_file );
$parent_theme_json_data = static::translate( $parent_theme_json_data, $wp_theme->parent()->get( 'TextDomain' ) );
$parent_theme = new WP_Theme_JSON_Gutenberg( $parent_theme_json_data );
@@ -383,7 +383,7 @@ public static function get_block_data() {
if (
isset( $block_type->supports['spacing']['blockGap']['__experimentalDefault'] ) &&
- null === _wp_array_get( $config, array( 'styles', 'blocks', $block_name, 'spacing', 'blockGap' ), null )
+ ! isset( $config['styles']['blocks'][ $block_name ]['spacing']['blockGap'] )
) {
// Ensure an empty placeholder value exists for the block, if it provides a default blockGap value.
// The real blockGap value to be used will be determined when the styles are rendered for output.
@@ -408,18 +408,18 @@ public static function get_block_data() {
/**
* When given an array, this will remove any keys with the name `//`.
*
- * @param array $array The array to filter.
+ * @param array $json_array The array to filter.
* @return array The filtered array.
*/
- private static function remove_json_comments( $array ) {
- unset( $array['//'] );
- foreach ( $array as $k => $v ) {
+ private static function remove_json_comments( $json_array ) {
+ unset( $json_array['//'] );
+ foreach ( $json_array as $k => $v ) {
if ( is_array( $v ) ) {
- $array[ $k ] = static::remove_json_comments( $v );
+ $json_array[ $k ] = static::remove_json_comments( $v );
}
}
- return $array;
+ return $json_array;
}
/**
@@ -670,6 +670,8 @@ public static function theme_has_support() {
* @return string The whole file path or empty if the file doesn't exist.
*/
protected static function get_file_path_from_theme( $file_name, $template = false ) {
+ // TODO: Remove this method from core on 6.3 release.
+ _deprecated_function( __METHOD__, '6.3.0' );
$path = $template ? get_template_directory() : get_stylesheet_directory();
$candidate = $path . '/' . $file_name;
diff --git a/lib/class-wp-theme-json-schema-gutenberg.php b/lib/class-wp-theme-json-schema-gutenberg.php
new file mode 100644
index 0000000000000..d11545751af36
--- /dev/null
+++ b/lib/class-wp-theme-json-schema-gutenberg.php
@@ -0,0 +1,202 @@
+ 'border.radius',
+ 'spacing.customMargin' => 'spacing.margin',
+ 'spacing.customPadding' => 'spacing.padding',
+ 'typography.customLineHeight' => 'typography.lineHeight',
+ );
+
+ /**
+ * Function that migrates a given theme.json structure to the last version.
+ *
+ * @since 5.9.0
+ *
+ * @param array $theme_json The structure to migrate.
+ *
+ * @return array The structure in the last version.
+ */
+ public static function migrate( $theme_json ) {
+ if ( ! isset( $theme_json['version'] ) ) {
+ $theme_json = array(
+ 'version' => WP_Theme_JSON::LATEST_SCHEMA,
+ );
+ }
+
+ if ( 1 === $theme_json['version'] ) {
+ $theme_json = self::migrate_v1_to_v2( $theme_json );
+ }
+
+ if ( 2 === $theme_json['version'] ) {
+ $theme_json = self::migrate_deprecated_lightbox_behaviors( $theme_json );
+ }
+
+ return $theme_json;
+ }
+
+ /**
+ * Removes the custom prefixes for a few properties
+ * that were part of v1:
+ *
+ * 'border.customRadius' => 'border.radius',
+ * 'spacing.customMargin' => 'spacing.margin',
+ * 'spacing.customPadding' => 'spacing.padding',
+ * 'typography.customLineHeight' => 'typography.lineHeight',
+ *
+ * @since 5.9.0
+ *
+ * @param array $old Data to migrate.
+ *
+ * @return array Data without the custom prefixes.
+ */
+ private static function migrate_v1_to_v2( $old ) {
+ // Copy everything.
+ $new = $old;
+
+ // Overwrite the things that changed.
+ if ( isset( $old['settings'] ) ) {
+ $new['settings'] = self::rename_paths( $old['settings'], self::V1_TO_V2_RENAMED_PATHS );
+ }
+
+ // Set the new version.
+ $new['version'] = 2;
+
+ return $new;
+ }
+
+
+ /**
+ * Migrate away from the previous syntax that used a top-level "behaviors" key
+ * in the `theme.json` to a new "lightbox" setting.
+ *
+ * This function SHOULD NOT be ported to Core!!!
+ *
+ * It is a temporary migration that will be removed in Gutenberg 17.0.0
+ *
+ * @since 16.7.0
+ *
+ * @param array $old Data with (potentially) behaviors.
+ * @return array Data with behaviors removed.
+ */
+ private static function migrate_deprecated_lightbox_behaviors( $old ) {
+ // Copy everything.
+ $new = $old;
+
+ // Migrate the old behaviors syntax to the new "lightbox" syntax.
+ if ( isset( $old['behaviors']['blocks']['core/image']['lightbox']['enabled'] ) ) {
+ _wp_array_set(
+ $new,
+ array( 'settings', 'blocks', 'core/image', 'lightbox', 'enabled' ),
+ $old['behaviors']['blocks']['core/image']['lightbox']['enabled']
+ );
+ }
+
+ // Migrate the behaviors setting to the new syntax. This setting controls
+ // whether the Lightbox UI shows up in the block editor.
+ if ( isset( $old['settings']['blocks']['core/image']['behaviors']['lightbox'] ) ) {
+ _wp_array_set(
+ $new,
+ array( 'settings', 'blocks', 'core/image', 'lightbox', 'allowEditing' ),
+ $old['settings']['blocks']['core/image']['behaviors']['lightbox']
+ );
+ }
+
+ return $new;
+ }
+
+ /**
+ * Processes the settings subtree.
+ *
+ * @since 5.9.0
+ *
+ * @param array $settings Array to process.
+ * @param array $paths_to_rename Paths to rename.
+ *
+ * @return array The settings in the new format.
+ */
+ private static function rename_paths( $settings, $paths_to_rename ) {
+ $new_settings = $settings;
+
+ // Process any renamed/moved paths within default settings.
+ self::rename_settings( $new_settings, $paths_to_rename );
+
+ // Process individual block settings.
+ if ( isset( $new_settings['blocks'] ) && is_array( $new_settings['blocks'] ) ) {
+ foreach ( $new_settings['blocks'] as &$block_settings ) {
+ self::rename_settings( $block_settings, $paths_to_rename );
+ }
+ }
+
+ return $new_settings;
+ }
+
+ /**
+ * Processes a settings array, renaming or moving properties.
+ *
+ * @since 5.9.0
+ *
+ * @param array $settings Reference to settings either defaults or an individual block's.
+ * @param array $paths_to_rename Paths to rename.
+ */
+ private static function rename_settings( &$settings, $paths_to_rename ) {
+ foreach ( $paths_to_rename as $original => $renamed ) {
+ $original_path = explode( '.', $original );
+ $renamed_path = explode( '.', $renamed );
+ $current_value = _wp_array_get( $settings, $original_path, null );
+
+ if ( null !== $current_value ) {
+ _wp_array_set( $settings, $renamed_path, $current_value );
+ self::unset_setting_by_path( $settings, $original_path );
+ }
+ }
+ }
+
+ /**
+ * Removes a property from within the provided settings by its path.
+ *
+ * @since 5.9.0
+ *
+ * @param array $settings Reference to the current settings array.
+ * @param array $path Path to the property to be removed.
+ */
+ private static function unset_setting_by_path( &$settings, $path ) {
+ $tmp_settings = &$settings; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+ $last_key = array_pop( $path );
+ foreach ( $path as $key ) {
+ $tmp_settings = &$tmp_settings[ $key ];
+ }
+
+ unset( $tmp_settings[ $last_key ] );
+ }
+}
diff --git a/lib/compat/wordpress-6.2/block-editor.php b/lib/compat/wordpress-6.2/block-editor.php
deleted file mode 100644
index 4df2b6b2652e0..0000000000000
--- a/lib/compat/wordpress-6.2/block-editor.php
+++ /dev/null
@@ -1,45 +0,0 @@
- get_theme_support( 'disable-custom-colors' ),
- 'disableCustomFontSizes' => get_theme_support( 'disable-custom-font-sizes' ),
- 'disableCustomGradients' => get_theme_support( 'disable-custom-gradients' ),
- 'disableLayoutStyles' => get_theme_support( 'disable-layout-styles' ),
- 'enableCustomLineHeight' => get_theme_support( 'custom-line-height' ),
- 'enableCustomSpacing' => get_theme_support( 'custom-spacing' ),
- 'enableCustomUnits' => get_theme_support( 'custom-units' ),
- );
-
- // Theme settings.
- $color_palette = current( (array) get_theme_support( 'editor-color-palette' ) );
- if ( false !== $color_palette ) {
- $theme_settings['colors'] = $color_palette;
- }
-
- $font_sizes = current( (array) get_theme_support( 'editor-font-sizes' ) );
- if ( false !== $font_sizes ) {
- $theme_settings['fontSizes'] = $font_sizes;
- }
-
- $gradient_presets = current( (array) get_theme_support( 'editor-gradient-presets' ) );
- if ( false !== $gradient_presets ) {
- $theme_settings['gradients'] = $gradient_presets;
- }
-
- return $theme_settings;
- }
-}
diff --git a/lib/compat/wordpress-6.2/block-patterns.php b/lib/compat/wordpress-6.2/block-patterns.php
deleted file mode 100644
index 12b19bdf4c54e..0000000000000
--- a/lib/compat/wordpress-6.2/block-patterns.php
+++ /dev/null
@@ -1,333 +0,0 @@
- _x( 'Banners', 'Block pattern category', 'gutenberg' ),
- )
- );
- register_block_pattern_category(
- 'buttons',
- array(
- 'label' => _x( 'Buttons', 'Block pattern category', 'gutenberg' ),
- 'description' => __( 'Patterns that contain buttons and call to actions.', 'gutenberg' ),
- )
- );
- register_block_pattern_category(
- 'columns',
- array(
- 'label' => _x( 'Columns', 'Block pattern category', 'gutenberg' ),
- 'description' => __( 'Multi-column patterns with more complex layouts.', 'gutenberg' ),
- )
- );
- register_block_pattern_category(
- 'text',
- array(
- 'label' => _x( 'Text', 'Block pattern category', 'gutenberg' ),
- 'description' => __( 'Patterns containing mostly text.', 'gutenberg' ),
- )
- );
- register_block_pattern_category(
- 'query',
- array(
- 'label' => _x( 'Posts', 'Block pattern category', 'gutenberg' ),
- 'description' => __( 'Display your latest posts in lists, grids or other layouts.', 'gutenberg' ),
- )
- );
- register_block_pattern_category(
- 'featured',
- array(
- 'label' => _x( 'Featured', 'Block pattern category', 'gutenberg' ),
- 'description' => __( 'A set of high quality curated patterns.', 'gutenberg' ),
- )
- );
-
- // Register new core block pattern categories.
- register_block_pattern_category(
- 'call-to-action',
- array(
- 'label' => _x( 'Call to Action', 'Block pattern category', 'gutenberg' ),
- 'description' => __( 'Sections whose purpose is to trigger a specific action.', 'gutenberg' ),
- )
- );
- register_block_pattern_category(
- 'team',
- array(
- 'label' => _x( 'Team', 'Block pattern category', 'gutenberg' ),
- 'description' => __( 'A variety of designs to display your team members.', 'gutenberg' ),
- )
- );
- register_block_pattern_category(
- 'testimonials',
- array(
- 'label' => _x( 'Testimonials', 'Block pattern category', 'gutenberg' ),
- 'description' => __( 'Share reviews and feedback about your brand/business.', 'gutenberg' ),
- )
- );
- register_block_pattern_category(
- 'services',
- array(
- 'label' => _x( 'Services', 'Block pattern category', 'gutenberg' ),
- 'description' => __( 'Briefly describe what your business does and how you can help.', 'gutenberg' ),
- )
- );
- register_block_pattern_category(
- 'contact',
- array(
- 'label' => _x( 'Contact', 'Block pattern category', 'gutenberg' ),
- 'description' => __( 'Display your contact information.', 'gutenberg' ),
- )
- );
- register_block_pattern_category(
- 'about',
- array(
- 'label' => _x( 'About', 'Block pattern category', 'gutenberg' ),
- 'description' => __( 'Introduce yourself.', 'gutenberg' ),
- )
- );
- register_block_pattern_category(
- 'portfolio',
- array(
- 'label' => _x( 'Portfolio', 'Block pattern category', 'gutenberg' ),
- 'description' => __( 'Showcase your latest work.', 'gutenberg' ),
- )
- );
- register_block_pattern_category(
- 'gallery',
- array(
- 'label' => _x( 'Gallery', 'Block pattern category', 'gutenberg' ),
- 'description' => __( 'Different layouts for displaying images.', 'gutenberg' ),
- )
- );
- register_block_pattern_category(
- 'media',
- array(
- 'label' => _x( 'Media', 'Block pattern category', 'gutenberg' ),
- 'description' => __( 'Different layouts containing video or audio.', 'gutenberg' ),
- )
- );
- register_block_pattern_category(
- 'posts',
- array(
- 'label' => _x( 'Posts', 'Block pattern category', 'gutenberg' ),
- 'description' => __( 'Display your latest posts in lists, grids or other layouts.', 'gutenberg' ),
- )
- );
- // Site building pattern categories.
- register_block_pattern_category(
- 'footer',
- array(
- 'label' => _x( 'Footers', 'Block pattern category', 'gutenberg' ),
- 'description' => __( 'A variety of footer designs displaying information and site navigation.', 'gutenberg' ),
- )
- );
- register_block_pattern_category(
- 'header',
- array(
- 'label' => _x( 'Headers', 'Block pattern category', 'gutenberg' ),
- 'description' => __( 'A variety of header designs displaying your site title and navigation.', 'gutenberg' ),
- )
- );
-}
-add_action( 'init', 'gutenberg_register_core_block_patterns_categories' );
-
-/**
- * Register any patterns that the active theme may provide under its
- * `./patterns/` directory. Each pattern is defined as a PHP file and defines
- * its metadata using plugin-style headers. The minimum required definition is:
- *
- * /**
- * * Title: My Pattern
- * * Slug: my-theme/my-pattern
- * *
- *
- * The output of the PHP source corresponds to the content of the pattern, e.g.:
- *
- *
- *
- * If applicable, this will collect from both parent and child theme.
- *
- * Other settable fields include:
- *
- * - Description
- * - Viewport Width
- * - Categories (comma-separated values)
- * - Keywords (comma-separated values)
- * - Block Types (comma-separated values)
- * - Post Types (comma-separated values)
- * - Inserter (yes/no)
- *
- * @since 6.0.0
- * @access private
- */
-function gutenberg_register_theme_block_patterns() {
- $default_headers = array(
- 'title' => 'Title',
- 'slug' => 'Slug',
- 'description' => 'Description',
- 'viewportWidth' => 'Viewport Width',
- 'categories' => 'Categories',
- 'keywords' => 'Keywords',
- 'blockTypes' => 'Block Types',
- 'postTypes' => 'Post Types',
- 'inserter' => 'Inserter',
- 'templateTypes' => 'Template Types',
- );
-
- /*
- * Register patterns for the active theme. If the theme is a child theme,
- * let it override any patterns from the parent theme that shares the same slug.
- */
- $themes = array();
- $wp_theme = wp_get_theme();
- if ( $wp_theme->parent() ) {
- $themes[] = $wp_theme->parent();
- }
- $themes[] = $wp_theme;
-
- foreach ( $themes as $theme ) {
- $dirpath = $theme->get_stylesheet_directory() . '/patterns/';
- if ( ! is_dir( $dirpath ) || ! is_readable( $dirpath ) ) {
- continue;
- }
- if ( file_exists( $dirpath ) ) {
- $files = glob( $dirpath . '*.php' );
- if ( $files ) {
- foreach ( $files as $file ) {
- $pattern_data = get_file_data( $file, $default_headers );
-
- if ( empty( $pattern_data['slug'] ) ) {
- _doing_it_wrong(
- '_register_theme_block_patterns',
- sprintf(
- /* translators: %s: file name. */
- __( 'Could not register file "%s" as a block pattern ("Slug" field missing)', 'gutenberg' ),
- $file
- ),
- '6.0.0'
- );
- continue;
- }
-
- if ( ! preg_match( '/^[A-z0-9\/_-]+$/', $pattern_data['slug'] ) ) {
- _doing_it_wrong(
- '_register_theme_block_patterns',
- sprintf(
- /* translators: %1s: file name; %2s: slug value found. */
- __( 'Could not register file "%1$s" as a block pattern (invalid slug "%2$s")', 'gutenberg' ),
- $file,
- $pattern_data['slug']
- ),
- '6.0.0'
- );
- }
-
- if ( WP_Block_Patterns_Registry::get_instance()->is_registered( $pattern_data['slug'] ) ) {
- continue;
- }
-
- // Title is a required property.
- if ( ! $pattern_data['title'] ) {
- _doing_it_wrong(
- '_register_theme_block_patterns',
- sprintf(
- /* translators: %1s: file name; %2s: slug value found. */
- __( 'Could not register file "%s" as a block pattern ("Title" field missing)', 'gutenberg' ),
- $file
- ),
- '6.0.0'
- );
- continue;
- }
-
- // For properties of type array, parse data as comma-separated.
- foreach ( array( 'categories', 'keywords', 'blockTypes', 'postTypes', 'templateTypes' ) as $property ) {
- if ( ! empty( $pattern_data[ $property ] ) ) {
- $pattern_data[ $property ] = array_filter(
- preg_split(
- '/[\s,]+/',
- (string) $pattern_data[ $property ]
- )
- );
- } else {
- unset( $pattern_data[ $property ] );
- }
- }
-
- // Parse properties of type int.
- foreach ( array( 'viewportWidth' ) as $property ) {
- if ( ! empty( $pattern_data[ $property ] ) ) {
- $pattern_data[ $property ] = (int) $pattern_data[ $property ];
- } else {
- unset( $pattern_data[ $property ] );
- }
- }
-
- // Parse properties of type bool.
- foreach ( array( 'inserter' ) as $property ) {
- if ( ! empty( $pattern_data[ $property ] ) ) {
- $pattern_data[ $property ] = in_array(
- strtolower( $pattern_data[ $property ] ),
- array( 'yes', 'true' ),
- true
- );
- } else {
- unset( $pattern_data[ $property ] );
- }
- }
-
- // Translate the pattern metadata.
- $text_domain = $theme->get( 'TextDomain' );
- //phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.NonSingularStringLiteralContext, WordPress.WP.I18n.NonSingularStringLiteralDomain, WordPress.WP.I18n.LowLevelTranslationFunction
- $pattern_data['title'] = translate_with_gettext_context( $pattern_data['title'], 'Pattern title', $text_domain );
- if ( ! empty( $pattern_data['description'] ) ) {
- //phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.NonSingularStringLiteralContext, WordPress.WP.I18n.NonSingularStringLiteralDomain, WordPress.WP.I18n.LowLevelTranslationFunction
- $pattern_data['description'] = translate_with_gettext_context( $pattern_data['description'], 'Pattern description', $text_domain );
- }
-
- // The actual pattern content is the output of the file.
- ob_start();
- include $file;
- $pattern_data['content'] = ob_get_clean();
- if ( ! $pattern_data['content'] ) {
- continue;
- }
-
- register_block_pattern( $pattern_data['slug'], $pattern_data );
- }
- }
- }
- }
-}
-remove_action( 'init', '_register_theme_block_patterns' );
-add_action( 'init', 'gutenberg_register_theme_block_patterns' );
-
-/**
- * Normalize the pattern from the API (snake_case) to the format expected by `register_block_pattern` (camelCase).
- *
- * @since 6.2.0
- *
- * @param array $pattern Pattern as returned from the Pattern Directory API.
- */
-function gutenberg_normalize_remote_pattern( $pattern ) {
- if ( isset( $pattern['block_types'] ) ) {
- $pattern['blockTypes'] = $pattern['block_types'];
- unset( $pattern['block_types'] );
- }
-
- if ( isset( $pattern['viewport_width'] ) ) {
- $pattern['viewportWidth'] = $pattern['viewport_width'];
- unset( $pattern['viewport_width'] );
- }
-
- return (array) $pattern;
-}
diff --git a/lib/compat/wordpress-6.2/block-template-utils.php b/lib/compat/wordpress-6.2/block-template-utils.php
deleted file mode 100644
index db9bf427e6b47..0000000000000
--- a/lib/compat/wordpress-6.2/block-template-utils.php
+++ /dev/null
@@ -1,97 +0,0 @@
- The template hierarchy.
- */
-function gutenberg_get_template_hierarchy( $slug, $is_custom = false, $template_prefix = '' ) {
- if ( 'index' === $slug ) {
- return array( 'index' );
- }
- if ( $is_custom ) {
- return array( 'page', 'singular', 'index' );
- }
- if ( 'front-page' === $slug ) {
- return array( 'front-page', 'home', 'index' );
- }
-
- $matches = array();
-
- $template_hierarchy = array( $slug );
- // Most default templates don't have `$template_prefix` assigned.
- if ( ! empty( $template_prefix ) ) {
- list( $type ) = explode( '-', $template_prefix );
- // We need these checks because we always add the `$slug` above.
- if ( ! in_array( $template_prefix, array( $slug, $type ), true ) ) {
- $template_hierarchy[] = $template_prefix;
- }
- if ( $slug !== $type ) {
- $template_hierarchy[] = $type;
- }
- } elseif ( preg_match( '/^(author|category|archive|tag|page)-.+$/', $slug, $matches ) ) {
- $template_hierarchy[] = $matches[1];
- } elseif ( preg_match( '/^(taxonomy|single)-(.+)$/', $slug, $matches ) ) {
- $type = $matches[1];
- $slug_remaining = $matches[2];
-
- $items = 'single' === $type ? get_post_types() : get_taxonomies();
- foreach ( $items as $item ) {
- if ( ! str_starts_with( $slug_remaining, $item ) ) {
- continue;
- }
-
- // If $slug_remaining is equal to $post_type or $taxonomy we have
- // the single-$post_type template or the taxonomy-$taxonomy template.
- if ( $slug_remaining === $item ) {
- $template_hierarchy[] = $type;
- break;
- }
-
- // If $slug_remaining is single-$post_type-$slug template.
- if ( strlen( $slug_remaining ) > strlen( $item ) + 1 ) {
- $template_hierarchy[] = "$type-$item";
- $template_hierarchy[] = $type;
- break;
- }
- }
- }
- // Handle `archive` template.
- if (
- str_starts_with( $slug, 'author' ) ||
- str_starts_with( $slug, 'taxonomy' ) ||
- str_starts_with( $slug, 'category' ) ||
- str_starts_with( $slug, 'tag' ) ||
- 'date' === $slug
- ) {
- $template_hierarchy[] = 'archive';
- }
- // Handle `single` template.
- if ( 'attachment' === $slug ) {
- $template_hierarchy[] = 'single';
- }
- // Handle `singular` template.
- if (
- str_starts_with( $slug, 'single' ) ||
- str_starts_with( $slug, 'page' ) ||
- 'attachment' === $slug
- ) {
- $template_hierarchy[] = 'singular';
- }
- $template_hierarchy[] = 'index';
- return $template_hierarchy;
-}
diff --git a/lib/compat/wordpress-6.2/blocks.php b/lib/compat/wordpress-6.2/blocks.php
deleted file mode 100644
index 94c6eaabcef9b..0000000000000
--- a/lib/compat/wordpress-6.2/blocks.php
+++ /dev/null
@@ -1,173 +0,0 @@
-= 6.2.
- *
- * @param string[] $attrs Array of allowed CSS attributes.
- * @return string[] CSS attributes.
- */
-function gutenberg_safe_style_attrs_6_2( $attrs ) {
- $attrs[] = 'position';
- $attrs[] = 'top';
- $attrs[] = 'right';
- $attrs[] = 'bottom';
- $attrs[] = 'left';
- $attrs[] = 'z-index';
- $attrs[] = 'box-shadow';
- $attrs[] = 'aspect-ratio';
-
- return $attrs;
-}
-add_filter( 'safe_style_css', 'gutenberg_safe_style_attrs_6_2' );
-
-/**
- * Helper function that constructs a WP_Query args array from
- * a `Query` block properties.
- *
- * It's used in QueryLoop, QueryPaginationNumbers and QueryPaginationNext blocks.
- *
- * `build_query_vars_from_query_block` was introduced in 5.8, for 6.1 we just need
- * to update that function and not create a new one.
- *
- * @param WP_Block $block Block instance.
- * @param int $page Current query's page.
- *
- * @return array Returns the constructed WP_Query arguments.
- */
-function gutenberg_build_query_vars_from_query_block( $block, $page ) {
- $query = array(
- 'post_type' => 'post',
- 'order' => 'DESC',
- 'orderby' => 'date',
- 'post__not_in' => array(),
- );
-
- if ( isset( $block->context['query'] ) ) {
- if ( ! empty( $block->context['query']['postType'] ) ) {
- $post_type_param = $block->context['query']['postType'];
- if ( is_post_type_viewable( $post_type_param ) ) {
- $query['post_type'] = $post_type_param;
- }
- }
- if ( isset( $block->context['query']['sticky'] ) && ! empty( $block->context['query']['sticky'] ) ) {
- $sticky = get_option( 'sticky_posts' );
- if ( 'only' === $block->context['query']['sticky'] ) {
- /**
- * Passing an empty array to post__in will return have_posts() as true (and all posts will be returned).
- * Logic should be used before hand to determine if WP_Query should be used in the event that the array
- * being passed to post__in is empty.
- *
- * @see https://core.trac.wordpress.org/ticket/28099
- */
- $query['post__in'] = ! empty( $sticky ) ? $sticky : array( 0 );
- $query['ignore_sticky_posts'] = 1;
- } else {
- $query['post__not_in'] = array_merge( $query['post__not_in'], $sticky );
- }
- }
- if ( ! empty( $block->context['query']['exclude'] ) ) {
- $excluded_post_ids = array_map( 'intval', $block->context['query']['exclude'] );
- $excluded_post_ids = array_filter( $excluded_post_ids );
- $query['post__not_in'] = array_merge( $query['post__not_in'], $excluded_post_ids );
- }
- if (
- isset( $block->context['query']['perPage'] ) &&
- is_numeric( $block->context['query']['perPage'] )
- ) {
- $per_page = absint( $block->context['query']['perPage'] );
- $offset = 0;
-
- if (
- isset( $block->context['query']['offset'] ) &&
- is_numeric( $block->context['query']['offset'] )
- ) {
- $offset = absint( $block->context['query']['offset'] );
- }
-
- $query['offset'] = ( $per_page * ( $page - 1 ) ) + $offset;
- $query['posts_per_page'] = $per_page;
- }
-
- // Migrate `categoryIds` and `tagIds` to `tax_query` for backwards compatibility.
- if ( ! empty( $block->context['query']['categoryIds'] ) || ! empty( $block->context['query']['tagIds'] ) ) {
- $tax_query = array();
- if ( ! empty( $block->context['query']['categoryIds'] ) ) {
- $tax_query[] = array(
- 'taxonomy' => 'category',
- 'terms' => array_filter( array_map( 'intval', $block->context['query']['categoryIds'] ) ),
- 'include_children' => false,
- );
- }
- if ( ! empty( $block->context['query']['tagIds'] ) ) {
- $tax_query[] = array(
- 'taxonomy' => 'post_tag',
- 'terms' => array_filter( array_map( 'intval', $block->context['query']['tagIds'] ) ),
- 'include_children' => false,
- );
- }
- $query['tax_query'] = $tax_query;
- }
- if ( ! empty( $block->context['query']['taxQuery'] ) ) {
- $query['tax_query'] = array();
- foreach ( $block->context['query']['taxQuery'] as $taxonomy => $terms ) {
- if ( is_taxonomy_viewable( $taxonomy ) && ! empty( $terms ) ) {
- $query['tax_query'][] = array(
- 'taxonomy' => $taxonomy,
- 'terms' => array_filter( array_map( 'intval', $terms ) ),
- 'include_children' => false,
- );
- }
- }
- }
- if (
- isset( $block->context['query']['order'] ) &&
- in_array( strtoupper( $block->context['query']['order'] ), array( 'ASC', 'DESC' ), true )
- ) {
- $query['order'] = strtoupper( $block->context['query']['order'] );
- }
- if ( isset( $block->context['query']['orderBy'] ) ) {
- $query['orderby'] = $block->context['query']['orderBy'];
- }
- if (
- isset( $block->context['query']['author'] ) &&
- (int) $block->context['query']['author'] > 0
- ) {
- $query['author'] = (int) $block->context['query']['author'];
- }
- if ( ! empty( $block->context['query']['search'] ) ) {
- $query['s'] = $block->context['query']['search'];
- }
- if ( ! empty( $block->context['query']['parents'] ) && is_post_type_hierarchical( $query['post_type'] ) ) {
- $query['post_parent__in'] = array_filter( array_map( 'intval', $block->context['query']['parents'] ) );
- }
- }
-
- /**
- * Filters the arguments which will be passed to `WP_Query` for the Query Loop Block.
- *
- * Anything to this filter should be compatible with the `WP_Query` API to form
- * the query context which will be passed down to the Query Loop Block's children.
- * This can help, for example, to include additional settings or meta queries not
- * directly supported by the core Query Loop Block, and extend its capabilities.
- *
- * Please note that this will only influence the query that will be rendered on the
- * front-end. The editor preview is not affected by this filter. Also, worth noting
- * that the editor preview uses the REST API, so, ideally, one should aim to provide
- * attributes which are also compatible with the REST API, in order to be able to
- * implement identical queries on both sides.
- *
- * @since 6.1.0
- *
- * @param array $query Array containing parameters for `WP_Query` as parsed by the block context.
- * @param WP_Block $block Block instance.
- * @param int $page Current query's page.
- */
- return apply_filters( 'query_loop_block_query_vars', $query, $block, $page );
-}
diff --git a/lib/compat/wordpress-6.2/class-gutenberg-rest-block-pattern-categories-controller.php b/lib/compat/wordpress-6.2/class-gutenberg-rest-block-pattern-categories-controller.php
deleted file mode 100644
index 3897a8945a5f9..0000000000000
--- a/lib/compat/wordpress-6.2/class-gutenberg-rest-block-pattern-categories-controller.php
+++ /dev/null
@@ -1,84 +0,0 @@
-get_fields_for_response( $request );
- $keys = array( 'name', 'label', 'description' );
- $data = array();
- foreach ( $keys as $key ) {
- if ( isset( $item[ $key ] ) && rest_is_field_included( $key, $fields ) ) {
- $data[ $key ] = $item[ $key ];
- }
- }
-
- $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
- $data = $this->add_additional_fields_to_object( $data, $request );
- $data = $this->filter_response_by_context( $data, $context );
- return rest_ensure_response( $data );
- }
-
- /**
- * Retrieves the block pattern category schema, conforming to JSON Schema.
- *
- * @since 6.0.0
- *
- * @return array Item schema data.
- */
- public function get_item_schema() {
- if ( $this->schema ) {
- return $this->add_additional_fields_schema( $this->schema );
- }
-
- $schema = array(
- '$schema' => 'http://json-schema.org/draft-04/schema#',
- 'title' => 'block-pattern-category',
- 'type' => 'object',
- 'properties' => array(
- 'name' => array(
- 'description' => __( 'The category name.', 'gutenberg' ),
- 'type' => 'string',
- 'readonly' => true,
- 'context' => array( 'view', 'edit', 'embed' ),
- ),
- 'label' => array(
- 'description' => __( 'The category label, in human readable format.', 'gutenberg' ),
- 'type' => 'string',
- 'readonly' => true,
- 'context' => array( 'view', 'edit', 'embed' ),
- ),
- 'description' => array(
- 'description' => __( 'The category description, in human readable format.', 'gutenberg' ),
- 'type' => 'string',
- 'readonly' => true,
- 'context' => array( 'view', 'edit', 'embed' ),
- ),
- ),
- );
-
- $this->schema = $schema;
-
- return $this->add_additional_fields_schema( $this->schema );
- }
-}
diff --git a/lib/compat/wordpress-6.2/class-gutenberg-rest-block-patterns-controller-6-2.php b/lib/compat/wordpress-6.2/class-gutenberg-rest-block-patterns-controller-6-2.php
deleted file mode 100644
index 80de88aa31a81..0000000000000
--- a/lib/compat/wordpress-6.2/class-gutenberg-rest-block-patterns-controller-6-2.php
+++ /dev/null
@@ -1,235 +0,0 @@
- 'call-to-action',
- 'columns' => 'text',
- 'query' => 'posts',
- );
-
- /**
- * Prepare a raw block pattern before it gets output in a REST API response.
- *
- * @since 6.0.0
- *
- * @param array $item Raw pattern as registered, before any changes.
- * @param WP_REST_Request $request Request object.
- * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
- */
- public function prepare_item_for_response( $item, $request ) {
- $fields = $this->get_fields_for_response( $request );
- $keys = array(
- 'name' => 'name',
- 'title' => 'title',
- 'description' => 'description',
- 'viewportWidth' => 'viewport_width',
- 'blockTypes' => 'block_types',
- 'postTypes' => 'post_types',
- 'categories' => 'categories',
- 'keywords' => 'keywords',
- 'content' => 'content',
- 'inserter' => 'inserter',
- 'templateTypes' => 'template_types',
- );
- $data = array();
- foreach ( $keys as $item_key => $rest_key ) {
- if ( isset( $item[ $item_key ] ) && rest_is_field_included( $rest_key, $fields ) ) {
- $data[ $rest_key ] = $item[ $item_key ];
- }
- }
- $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
- $data = $this->add_additional_fields_to_object( $data, $request );
- $data = $this->filter_response_by_context( $data, $context );
- return rest_ensure_response( $data );
- }
-
- /**
- * Retrieves the block pattern schema, conforming to JSON Schema.
- *
- * @since 6.0.0
- * @since 6.1.0 Added `post_types` property.
- *
- * @return array Item schema data.
- */
- public function get_item_schema() {
- if ( $this->schema ) {
- return $this->add_additional_fields_schema( $this->schema );
- }
-
- $schema = array(
- '$schema' => 'http://json-schema.org/draft-04/schema#',
- 'title' => 'block-pattern',
- 'type' => 'object',
- 'properties' => array(
- 'name' => array(
- 'description' => __( 'The pattern name.', 'gutenberg' ),
- 'type' => 'string',
- 'readonly' => true,
- 'context' => array( 'view', 'edit', 'embed' ),
- ),
- 'title' => array(
- 'description' => __( 'The pattern title, in human readable format.', 'gutenberg' ),
- 'type' => 'string',
- 'readonly' => true,
- 'context' => array( 'view', 'edit', 'embed' ),
- ),
- 'description' => array(
- 'description' => __( 'The pattern detailed description.', 'gutenberg' ),
- 'type' => 'string',
- 'readonly' => true,
- 'context' => array( 'view', 'edit', 'embed' ),
- ),
- 'viewport_width' => array(
- 'description' => __( 'The pattern viewport width for inserter preview.', 'gutenberg' ),
- 'type' => 'number',
- 'readonly' => true,
- 'context' => array( 'view', 'edit', 'embed' ),
- ),
- 'block_types' => array(
- 'description' => __( 'Block types that the pattern is intended to be used with.', 'gutenberg' ),
- 'type' => 'array',
- 'readonly' => true,
- 'context' => array( 'view', 'edit', 'embed' ),
- ),
- 'post_types' => array(
- 'description' => __( 'An array of post types that the pattern is restricted to be used with.', 'gutenberg' ),
- 'type' => 'array',
- 'readonly' => true,
- 'context' => array( 'view', 'edit', 'embed' ),
- ),
- 'categories' => array(
- 'description' => __( 'The pattern category slugs.', 'gutenberg' ),
- 'type' => 'array',
- 'readonly' => true,
- 'context' => array( 'view', 'edit', 'embed' ),
- ),
- 'keywords' => array(
- 'description' => __( 'The pattern keywords.', 'gutenberg' ),
- 'type' => 'array',
- 'readonly' => true,
- 'context' => array( 'view', 'edit', 'embed' ),
- ),
- 'template_types' => array(
- 'description' => __( 'An array of template types where the pattern fits.', 'gutenberg' ),
- 'type' => 'array',
- 'readonly' => true,
- 'context' => array( 'view', 'edit', 'embed' ),
- ),
- 'content' => array(
- 'description' => __( 'The pattern content.', 'gutenberg' ),
- 'type' => 'string',
- 'readonly' => true,
- 'context' => array( 'view', 'edit', 'embed' ),
- ),
- 'inserter' => array(
- 'description' => __( 'Determines whether the pattern is visible in inserter.', 'gutenberg' ),
- 'type' => 'boolean',
- 'readonly' => true,
- 'context' => array( 'view', 'edit', 'embed' ),
- ),
- ),
- );
-
- $this->schema = $schema;
-
- return $this->add_additional_fields_schema( $this->schema );
- }
-
- /**
- * Registers the routes for the objects of the controller.
- *
- * @since 6.0.0
- */
- public function register_routes() {
- register_rest_route(
- $this->namespace,
- '/' . $this->rest_base,
- array(
- array(
- 'methods' => WP_REST_Server::READABLE,
- 'callback' => array( $this, 'get_items' ),
- 'permission_callback' => array( $this, 'get_items_permissions_check' ),
- ),
- 'schema' => array( $this, 'get_public_item_schema' ),
- ),
- true
- );
- }
- /**
- * Retrieves all block patterns.
- *
- * @since 6.0.0
- * @since 6.2.0 Added migration for old core pattern categories to the new ones.
- *
- * @param WP_REST_Request $request Full details about the request.
- * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
- */
- public function get_items( $request ) {
- if ( ! $this->remote_patterns_loaded ) {
- // Load block patterns from w.org.
- gutenberg_load_remote_block_patterns(); // Patterns with the `core` keyword.
- gutenberg_load_remote_featured_patterns(); // Patterns in the `featured` category.
- gutenberg_register_remote_theme_patterns(); // Patterns requested by current theme.
-
- $this->remote_patterns_loaded = true;
- }
-
- $response = array();
- $patterns = WP_Block_Patterns_Registry::get_instance()->get_all_registered();
- foreach ( $patterns as $pattern ) {
- $migrated_pattern = $this->migrate_pattern_categories( $pattern );
- $prepared_pattern = $this->prepare_item_for_response( $migrated_pattern, $request );
- $response[] = $this->prepare_response_for_collection( $prepared_pattern );
- }
- return rest_ensure_response( $response );
- }
-
- /**
- * Migrates old core pattern categories to new ones.
- *
- * Core pattern categories are being revamped and we need to handle the migration
- * to the new ones and ensure backwards compatibility.
- *
- * @since 6.2.0
- *
- * @param array $pattern Raw pattern as registered, before applying any changes.
- * @return array Migrated pattern.
- */
- protected function migrate_pattern_categories( $pattern ) {
- if ( isset( $pattern['categories'] ) && is_array( $pattern['categories'] ) ) {
- foreach ( $pattern['categories'] as $i => $category ) {
- if ( array_key_exists( $category, static::$categories_migration ) ) {
- $pattern['categories'][ $i ] = static::$categories_migration[ $category ];
- }
- }
- }
- return $pattern;
- }
-}
diff --git a/lib/compat/wordpress-6.2/class-gutenberg-rest-pattern-directory-controller-6-2.php b/lib/compat/wordpress-6.2/class-gutenberg-rest-pattern-directory-controller-6-2.php
deleted file mode 100644
index 7043140c23fcf..0000000000000
--- a/lib/compat/wordpress-6.2/class-gutenberg-rest-pattern-directory-controller-6-2.php
+++ /dev/null
@@ -1,122 +0,0 @@
- true,
- 'order' => true,
- 'orderby' => true,
- 'page' => true,
- 'per_page' => true,
- 'search' => true,
- 'slug' => true,
- );
-
- $query_args = array_intersect_key( $request->get_params(), $valid_query_args );
-
- $query_args['locale'] = get_user_locale();
- $query_args['wp-version'] = $wp_version; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable -- it's defined in `version.php` above.
- $query_args['gutenberg-version'] = $gutenberg_data['Version'];
- $query_args['pattern-categories'] = isset( $request['category'] ) ? $request['category'] : false;
- $query_args['pattern-keywords'] = isset( $request['keyword'] ) ? $request['keyword'] : false;
-
- $query_args = array_filter( $query_args );
-
- $transient_key = $this->get_transient_key( $query_args );
-
- /*
- * Use network-wide transient to improve performance. The locale is the only site
- * configuration that affects the response, and it's included in the transient key.
- */
- $raw_patterns = get_site_transient( $transient_key );
-
- if ( ! $raw_patterns ) {
- $api_url = 'http://api.wordpress.org/patterns/1.0/?' . build_query( $query_args );
- if ( wp_http_supports( array( 'ssl' ) ) ) {
- $api_url = set_url_scheme( $api_url, 'https' );
- }
-
- /*
- * Default to a short TTL, to mitigate cache stampedes on high-traffic sites.
- * This assumes that most errors will be short-lived, e.g., packet loss that causes the
- * first request to fail, but a follow-up one will succeed. The value should be high
- * enough to avoid stampedes, but low enough to not interfere with users manually
- * re-trying a failed request.
- */
- $cache_ttl = 5;
- $wporg_response = wp_remote_get( $api_url );
- $raw_patterns = json_decode( wp_remote_retrieve_body( $wporg_response ) );
-
- if ( is_wp_error( $wporg_response ) ) {
- $raw_patterns = $wporg_response;
-
- } elseif ( ! is_array( $raw_patterns ) ) {
- // HTTP request succeeded, but response data is invalid.
- $raw_patterns = new WP_Error(
- 'pattern_api_failed',
- sprintf(
- /* translators: %s: Support forums URL. */
- __( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server’s configuration. If you continue to have problems, please try the support forums.', 'gutenberg' ),
- __( 'https://wordpress.org/support/forums/', 'gutenberg' )
- ),
- array(
- 'response' => wp_remote_retrieve_body( $wporg_response ),
- )
- );
-
- } else {
- // Response has valid data.
- $cache_ttl = HOUR_IN_SECONDS;
- }
-
- set_site_transient( $transient_key, $raw_patterns, $cache_ttl );
- }
-
- if ( is_wp_error( $raw_patterns ) ) {
- $raw_patterns->add_data( array( 'status' => 500 ) );
-
- return $raw_patterns;
- }
-
- $response = array();
-
- if ( $raw_patterns ) {
- foreach ( $raw_patterns as $pattern ) {
- $response[] = $this->prepare_response_for_collection(
- $this->prepare_item_for_response( $pattern, $request )
- );
- }
- }
-
- return new WP_REST_Response( $response );
- }
-}
diff --git a/lib/compat/wordpress-6.2/default-filters.php b/lib/compat/wordpress-6.2/default-filters.php
deleted file mode 100644
index 32a7a33157a74..0000000000000
--- a/lib/compat/wordpress-6.2/default-filters.php
+++ /dev/null
@@ -1,18 +0,0 @@
-name = $name;
- $this->value_starts_at = $value_start;
- $this->value_length = $value_length;
- $this->start = $start;
- $this->end = $end;
- $this->is_true = $is_true;
- }
-}
diff --git a/lib/compat/wordpress-6.2/html-api/class-wp-html-span.php b/lib/compat/wordpress-6.2/html-api/class-wp-html-span.php
deleted file mode 100644
index e38bc55192317..0000000000000
--- a/lib/compat/wordpress-6.2/html-api/class-wp-html-span.php
+++ /dev/null
@@ -1,56 +0,0 @@
-start = $start;
- $this->end = $end;
- }
-}
diff --git a/lib/compat/wordpress-6.2/html-api/class-wp-html-tag-processor.php b/lib/compat/wordpress-6.2/html-api/class-wp-html-tag-processor.php
deleted file mode 100644
index d61180074f608..0000000000000
--- a/lib/compat/wordpress-6.2/html-api/class-wp-html-tag-processor.php
+++ /dev/null
@@ -1,2291 +0,0 @@
- "c" not " c".
- * This would increase the size of the changes for some operations but leave more
- * natural-looking output HTML.
- * - Decode HTML character references within class names when matching. E.g. match having
- * class `1<"2` needs to recognize `class="1<"2"`. Currently the Tag Processor
- * will fail to find the right tag if the class name is encoded as such.
- * - Properly decode HTML character references in `get_attribute()`. PHP's
- * `html_entity_decode()` is wrong in a couple ways: it doesn't account for the
- * no-ambiguous-ampersand rule, and it improperly handles the way semicolons may
- * or may not terminate a character reference.
- *
- * @package WordPress
- * @subpackage HTML-API
- * @since 6.2.0
- */
-
-if ( class_exists( 'WP_HTML_Tag_Processor' ) ) {
- return;
-}
-
-/**
- * Modifies attributes in an HTML document for tags matching a query.
- *
- * ## Usage
- *
- * Use of this class requires three steps:
- *
- * 1. Create a new class instance with your input HTML document.
- * 2. Find the tag(s) you are looking for.
- * 3. Request changes to the attributes in those tag(s).
- *
- * Example:
- *
- * $tags = new WP_HTML_Tag_Processor( $html );
- * if ( $tags->next_tag( 'option' ) ) {
- * $tags->set_attribute( 'selected', true );
- * }
- *
- * ### Finding tags
- *
- * The `next_tag()` function moves the internal cursor through
- * your input HTML document until it finds a tag meeting any of
- * the supplied restrictions in the optional query argument. If
- * no argument is provided then it will find the next HTML tag,
- * regardless of what kind it is.
- *
- * If you want to _find whatever the next tag is_:
- *
- * $tags->next_tag();
- *
- * | Goal | Query |
- * |-----------------------------------------------------------|---------------------------------------------------------------------------------|
- * | Find any tag. | `$tags->next_tag();` |
- * | Find next image tag. | `$tags->next_tag( array( 'tag_name' => 'img' ) );` |
- * | Find next image tag (without passing the array). | `$tags->next_tag( 'img' );` |
- * | Find next tag containing the `fullwidth` CSS class. | `$tags->next_tag( array( 'class_name' => 'fullwidth' ) );` |
- * | Find next image tag containing the `fullwidth` CSS class. | `$tags->next_tag( array( 'tag_name' => 'img', 'class_name' => 'fullwidth' ) );` |
- *
- * If a tag was found meeting your criteria then `next_tag()`
- * will return `true` and you can proceed to modify it. If it
- * returns `false`, however, it failed to find the tag and
- * moved the cursor to the end of the file.
- *
- * Once the cursor reaches the end of the file the processor
- * is done and if you want to reach an earlier tag you will
- * need to recreate the processor and start over, as it's
- * unable to back up or move in reverse.
- *
- * See the section on bookmarks for an exception to this
- * no-backing-up rule.
- *
- * #### Custom queries
- *
- * Sometimes it's necessary to further inspect an HTML tag than
- * the query syntax here permits. In these cases one may further
- * inspect the search results using the read-only functions
- * provided by the processor or external state or variables.
- *
- * Example:
- *
- * // Paint up to the first five DIV or SPAN tags marked with the "jazzy" style.
- * $remaining_count = 5;
- * while ( $remaining_count > 0 && $tags->next_tag() ) {
- * if (
- * ( 'DIV' === $tags->get_tag() || 'SPAN' === $tags->get_tag() ) &&
- * 'jazzy' === $tags->get_attribute( 'data-style' )
- * ) {
- * $tags->add_class( 'theme-style-everest-jazz' );
- * $remaining_count--;
- * }
- * }
- *
- * `get_attribute()` will return `null` if the attribute wasn't present
- * on the tag when it was called. It may return `""` (the empty string)
- * in cases where the attribute was present but its value was empty.
- * For boolean attributes, those whose name is present but no value is
- * given, it will return `true` (the only way to set `false` for an
- * attribute is to remove it).
- *
- * ### Modifying HTML attributes for a found tag
- *
- * Once you've found the start of an opening tag you can modify
- * any number of the attributes on that tag. You can set a new
- * value for an attribute, remove the entire attribute, or do
- * nothing and move on to the next opening tag.
- *
- * Example:
- *
- * if ( $tags->next_tag( array( 'class' => 'wp-group-block' ) ) ) {
- * $tags->set_attribute( 'title', 'This groups the contained content.' );
- * $tags->remove_attribute( 'data-test-id' );
- * }
- *
- * If `set_attribute()` is called for an existing attribute it will
- * overwrite the existing value. Similarly, calling `remove_attribute()`
- * for a non-existing attribute has no effect on the document. Both
- * of these methods are safe to call without knowing if a given attribute
- * exists beforehand.
- *
- * ### Modifying CSS classes for a found tag
- *
- * The tag processor treats the `class` attribute as a special case.
- * Because it's a common operation to add or remove CSS classes, this
- * interface adds helper methods to make that easier.
- *
- * As with attribute values, adding or removing CSS classes is a safe
- * operation that doesn't require checking if the attribute or class
- * exists before making changes. If removing the only class then the
- * entire `class` attribute will be removed.
- *
- * Example:
- *
- * // from `Yippee!`
- * // to `Yippee!`
- * $tags->add_class( 'is-active' );
- *
- * // from `Yippee!`
- * // to `Yippee!`
- * $tags->add_class( 'is-active' );
- *
- * // from `Yippee!`
- * // to `Yippee!`
- * $tags->add_class( 'is-active' );
- *
- * // from ``
- * // to `
- * $tags->remove_class( 'rugby' );
- *
- * // from ``
- * // to `
- * $tags->remove_class( 'rugby' );
- *
- * // from ``
- * // to `
- * $tags->remove_class( 'rugby' );
- *
- * When class changes are enqueued but a direct change to `class` is made via
- * `set_attribute` then the changes to `set_attribute` (or `remove_attribute`)
- * will take precedence over those made through `add_class` and `remove_class`.
- *
- * ### Bookmarks
- *
- * While scanning through the input HTMl document it's possible to set
- * a named bookmark when a particular tag is found. Later on, after
- * continuing to scan other tags, it's possible to `seek` to one of
- * the set bookmarks and then proceed again from that point forward.
- *
- * Because bookmarks create processing overhead one should avoid
- * creating too many of them. As a rule, create only bookmarks
- * of known string literal names; avoid creating "mark_{$index}"
- * and so on. It's fine from a performance standpoint to create a
- * bookmark and update it frequently, such as within a loop.
- *
- * $total_todos = 0;
- * while ( $p->next_tag( array( 'tag_name' => 'UL', 'class_name' => 'todo' ) ) ) {
- * $p->set_bookmark( 'list-start' );
- * while ( $p->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
- * if ( 'UL' === $p->get_tag() && $p->is_tag_closer() ) {
- * $p->set_bookmark( 'list-end' );
- * $p->seek( 'list-start' );
- * $p->set_attribute( 'data-contained-todos', (string) $total_todos );
- * $total_todos = 0;
- * $p->seek( 'list-end' );
- * break;
- * }
- *
- * if ( 'LI' === $p->get_tag() && ! $p->is_tag_closer() ) {
- * $total_todos++;
- * }
- * }
- * }
- *
- * ## Design and limitations
- *
- * The Tag Processor is designed to linearly scan HTML documents and tokenize
- * HTML tags and their attributes. It's designed to do this as efficiently as
- * possible without compromising parsing integrity. Therefore it will be
- * slower than some methods of modifying HTML, such as those incorporating
- * over-simplified PCRE patterns, but will not introduce the defects and
- * failures that those methods bring in, which lead to broken page renders
- * and often to security vulnerabilities. On the other hand, it will be faster
- * than full-blown HTML parsers such as DOMDocument and use considerably
- * less memory. It requires a negligible memory overhead, enough to consider
- * it a zero-overhead system.
- *
- * The performance characteristics are maintained by avoiding tree construction
- * and semantic cleanups which are specified in HTML5. Because of this, for
- * example, it's not possible for the Tag Processor to associate any given
- * opening tag with its corresponding closing tag, or to return the inner markup
- * inside an element. Systems may be built on top of the Tag Processor to do
- * this, but the Tag Processor is and should be constrained so it can remain an
- * efficient, low-level, and reliable HTML scanner.
- *
- * The Tag Processor's design incorporates a "garbage-in-garbage-out" philosophy.
- * HTML5 specifies that certain invalid content be transformed into different forms
- * for display, such as removing null bytes from an input document and replacing
- * invalid characters with the Unicode replacement character U+FFFD ļæ½. Where errors
- * or transformations exist within the HTML5 specification, the Tag Processor leaves
- * those invalid inputs untouched, passing them through to the final browser to handle.
- * While this implies that certain operations will be non-spec-compliant, such as
- * reading the value of an attribute with invalid content, it also preserves a
- * simplicity and efficiency for handling those error cases.
- *
- * Most operations within the Tag Processor are designed to minimize the difference
- * between an input and output document for any given change. For example, the
- * `add_class` and `remove_class` methods preserve whitespace and the class ordering
- * within the `class` attribute; and when encountering tags with duplicated attributes,
- * the Tag Processor will leave those invalid duplicate attributes where they are but
- * update the proper attribute which the browser will read for parsing its value. An
- * exception to this rule is that all attribute updates store their values as
- * double-quoted strings, meaning that attributes on input with single-quoted or
- * unquoted values will appear in the output with double-quotes.
- *
- * @since 6.2.0
- */
-class WP_HTML_Tag_Processor {
- /**
- * The maximum number of bookmarks allowed to exist at
- * any given time.
- *
- * @since 6.2.0
- * @var int
- *
- * @see WP_HTML_Tag_Processor::set_bookmark()
- */
- const MAX_BOOKMARKS = 10;
-
- /**
- * Maximum number of times seek() can be called.
- * Prevents accidental infinite loops.
- *
- * @since 6.2.0
- * @var int
- *
- * @see WP_HTML_Tag_Processor::seek()
- */
- const MAX_SEEK_OPS = 1000;
-
- /**
- * The HTML document to parse.
- *
- * @since 6.2.0
- * @var string
- */
- protected $html;
-
- /**
- * The last query passed to next_tag().
- *
- * @since 6.2.0
- * @var array|null
- */
- private $last_query;
-
- /**
- * The tag name this processor currently scans for.
- *
- * @since 6.2.0
- * @var string|null
- */
- private $sought_tag_name;
-
- /**
- * The CSS class name this processor currently scans for.
- *
- * @since 6.2.0
- * @var string|null
- */
- private $sought_class_name;
-
- /**
- * The match offset this processor currently scans for.
- *
- * @since 6.2.0
- * @var int|null
- */
- private $sought_match_offset;
-
- /**
- * Whether to visit tag closers, e.g. , when walking an input document.
- *
- * @since 6.2.0
- * @var bool
- */
- private $stop_on_tag_closers;
-
- /**
- * How many bytes from the original HTML document have been read and parsed.
- *
- * This value points to the latest byte offset in the input document which
- * has been already parsed. It is the internal cursor for the Tag Processor
- * and updates while scanning through the HTML tokens.
- *
- * @since 6.2.0
- * @var int
- */
- private $bytes_already_parsed = 0;
-
- /**
- * Byte offset in input document where current tag name starts.
- *
- * Example:
- *
- *
...
- * 01234
- * - tag name starts at 1
- *
- * @since 6.2.0
- * @var int|null
- */
- private $tag_name_starts_at;
-
- /**
- * Byte length of current tag name.
- *
- * Example:
- *
- *
...
- * 01234
- * --- tag name length is 3
- *
- * @since 6.2.0
- * @var int|null
- */
- private $tag_name_length;
-
- /**
- * Byte offset in input document where current tag token ends.
- *
- * Example:
- *
- *
...
- * 0 1 |
- * 01234567890123456
- * --- tag name ends at 14
- *
- * @since 6.2.0
- * @var int|null
- */
- private $tag_ends_at;
-
- /**
- * Whether the current tag is an opening tag, e.g.
, or a closing tag, e.g.
.
- *
- * @var bool
- */
- private $is_closing_tag;
-
- /**
- * Lazily-built index of attributes found within an HTML tag, keyed by the attribute name.
- *
- * Example:
- *
- * // Supposing the parser is working through this content
- * // and stops after recognizing the `id` attribute.
- * //
- * // ^ parsing will continue from this point.
- * $this->attributes = array(
- * 'id' => new WP_HTML_Attribute_Match( 'id', null, 6, 17 )
- * );
- *
- * // When picking up parsing again, or when asking to find the
- * // `class` attribute we will continue and add to this array.
- * $this->attributes = array(
- * 'id' => new WP_HTML_Attribute_Match( 'id', null, 6, 17 ),
- * 'class' => new WP_HTML_Attribute_Match( 'class', 'outline', 18, 32 )
- * );
- *
- * // Note that only the `class` attribute value is stored in the index.
- * // That's because it is the only value used by this class at the moment.
- *
- * @since 6.2.0
- * @var WP_HTML_Attribute_Token[]
- */
- private $attributes = array();
-
- /**
- * Which class names to add or remove from a tag.
- *
- * These are tracked separately from attribute updates because they are
- * semantically distinct, whereas this interface exists for the common
- * case of adding and removing class names while other attributes are
- * generally modified as with DOM `setAttribute` calls.
- *
- * When modifying an HTML document these will eventually be collapsed
- * into a single `set_attribute( 'class', $changes )` call.
- *
- * Example:
- *
- * // Add the `wp-block-group` class, remove the `wp-group` class.
- * $classname_updates = array(
- * // Indexed by a comparable class name.
- * 'wp-block-group' => WP_HTML_Tag_Processor::ADD_CLASS,
- * 'wp-group' => WP_HTML_Tag_Processor::REMOVE_CLASS
- * );
- *
- * @since 6.2.0
- * @var bool[]
- */
- private $classname_updates = array();
-
- /**
- * Tracks a semantic location in the original HTML which
- * shifts with updates as they are applied to the document.
- *
- * @since 6.2.0
- * @var WP_HTML_Span[]
- */
- protected $bookmarks = array();
-
- const ADD_CLASS = true;
- const REMOVE_CLASS = false;
- const SKIP_CLASS = null;
-
- /**
- * Lexical replacements to apply to input HTML document.
- *
- * "Lexical" in this class refers to the part of this class which
- * operates on pure text _as text_ and not as HTML. There's a line
- * between the public interface, with HTML-semantic methods like
- * `set_attribute` and `add_class`, and an internal state that tracks
- * text offsets in the input document.
- *
- * When higher-level HTML methods are called, those have to transform their
- * operations (such as setting an attribute's value) into text diffing
- * operations (such as replacing the sub-string from indices A to B with
- * some given new string). These text-diffing operations are the lexical
- * updates.
- *
- * As new higher-level methods are added they need to collapse their
- * operations into these lower-level lexical updates since that's the
- * Tag Processor's internal language of change. Any code which creates
- * these lexical updates must ensure that they do not cross HTML syntax
- * boundaries, however, so these should never be exposed outside of this
- * class or any classes which intentionally expand its functionality.
- *
- * These are enqueued while editing the document instead of being immediately
- * applied to avoid processing overhead, string allocations, and string
- * copies when applying many updates to a single document.
- *
- * Example:
- *
- * // Replace an attribute stored with a new value, indices
- * // sourced from the lazily-parsed HTML recognizer.
- * $start = $attributes['src']->start;
- * $end = $attributes['src']->end;
- * $modifications[] = new WP_HTML_Text_Replacement( $start, $end, $new_value );
- *
- * // Correspondingly, something like this will appear in this array.
- * $lexical_updates = array(
- * WP_HTML_Text_Replacement( 14, 28, 'https://my-site.my-domain/wp-content/uploads/2014/08/kittens.jpg' )
- * );
- *
- * @since 6.2.0
- * @var WP_HTML_Text_Replacement[]
- */
- protected $lexical_updates = array();
-
- /**
- * Tracks and limits `seek()` calls to prevent accidental infinite loops.
- *
- * @since 6.2.0
- * @var int
- *
- * @see WP_HTML_Tag_Processor::seek()
- */
- protected $seek_count = 0;
-
- /**
- * Constructor.
- *
- * @since 6.2.0
- *
- * @param string $html HTML to process.
- */
- public function __construct( $html ) {
- $this->html = $html;
- }
-
- /**
- * Finds the next tag matching the $query.
- *
- * @since 6.2.0
- *
- * @param array|string|null $query {
- * Optional. Which tag name to find, having which class, etc. Default is to find any tag.
- *
- * @type string|null $tag_name Which tag to find, or `null` for "any tag."
- * @type int|null $match_offset Find the Nth tag matching all search criteria.
- * 1 for "first" tag, 3 for "third," etc.
- * Defaults to first tag.
- * @type string|null $class_name Tag must contain this whole class name to match.
- * @type string|null $tag_closers "visit" or "skip": whether to stop on tag closers, e.g.
.
- * }
- * @return boolean Whether a tag was matched.
- */
- public function next_tag( $query = null ) {
- $this->parse_query( $query );
- $already_found = 0;
-
- do {
- if ( $this->bytes_already_parsed >= strlen( $this->html ) ) {
- return false;
- }
-
- // Find the next tag if it exists.
- if ( false === $this->parse_next_tag() ) {
- $this->bytes_already_parsed = strlen( $this->html );
-
- return false;
- }
-
- // Parse all of its attributes.
- while ( $this->parse_next_attribute() ) {
- continue;
- }
-
- // Ensure that the tag closes before the end of the document.
- $tag_ends_at = strpos( $this->html, '>', $this->bytes_already_parsed );
- if ( false === $tag_ends_at ) {
- return false;
- }
- $this->tag_ends_at = $tag_ends_at;
- $this->bytes_already_parsed = $tag_ends_at;
-
- // Finally, check if the parsed tag and its attributes match the search query.
- if ( $this->matches() ) {
- ++$already_found;
- }
-
- /*
- * For non-DATA sections which might contain text that looks like HTML tags but
- * isn't, scan with the appropriate alternative mode. Looking at the first letter
- * of the tag name as a pre-check avoids a string allocation when it's not needed.
- */
- $t = $this->html[ $this->tag_name_starts_at ];
- if ( ! $this->is_closing_tag && ( 's' === $t || 'S' === $t || 't' === $t || 'T' === $t ) ) {
- $tag_name = $this->get_tag();
-
- if ( 'SCRIPT' === $tag_name && ! $this->skip_script_data() ) {
- $this->bytes_already_parsed = strlen( $this->html );
- return false;
- } elseif (
- ( 'TEXTAREA' === $tag_name || 'TITLE' === $tag_name ) &&
- ! $this->skip_rcdata( $tag_name )
- ) {
- $this->bytes_already_parsed = strlen( $this->html );
- return false;
- }
- }
- } while ( $already_found < $this->sought_match_offset );
-
- return true;
- }
-
-
- /**
- * Sets a bookmark in the HTML document.
- *
- * Bookmarks represent specific places or tokens in the HTML
- * document, such as a tag opener or closer. When applying
- * edits to a document, such as setting an attribute, the
- * text offsets of that token may shift; the bookmark is
- * kept updated with those shifts and remains stable unless
- * the entire span of text in which the token sits is removed.
- *
- * Release bookmarks when they are no longer needed.
- *
- * Example:
- *
- *
Surprising fact you may not know!
- * ^ ^
- * \-|-- this `H2` opener bookmark tracks the token
- *
- *
Surprising fact you may noā¦
- * ^ ^
- * \-|-- it shifts with edits
- *
- * Bookmarks provide the ability to seek to a previously-scanned
- * place in the HTML document. This avoids the need to re-scan
- * the entire document.
- *
- * Example:
- *
- *
One
Two
Three
- * ^^^^
- * want to note this last item
- *
- * $p = new WP_HTML_Tag_Processor( $html );
- * $in_list = false;
- * while ( $p->next_tag( array( 'tag_closers' => $in_list ? 'visit' : 'skip' ) ) ) {
- * if ( 'UL' === $p->get_tag() ) {
- * if ( $p->is_tag_closer() ) {
- * $in_list = false;
- * $p->set_bookmark( 'resume' );
- * if ( $p->seek( 'last-li' ) ) {
- * $p->add_class( 'last-li' );
- * }
- * $p->seek( 'resume' );
- * $p->release_bookmark( 'last-li' );
- * $p->release_bookmark( 'resume' );
- * } else {
- * $in_list = true;
- * }
- * }
- *
- * if ( 'LI' === $p->get_tag() ) {
- * $p->set_bookmark( 'last-li' );
- * }
- * }
- *
- * Bookmarks intentionally hide the internal string offsets
- * to which they refer. They are maintained internally as
- * updates are applied to the HTML document and therefore
- * retain their "position" - the location to which they
- * originally pointed. The inability to use bookmarks with
- * functions like `substr` is therefore intentional to guard
- * against accidentally breaking the HTML.
- *
- * Because bookmarks allocate memory and require processing
- * for every applied update, they are limited and require
- * a name. They should not be created with programmatically-made
- * names, such as "li_{$index}" with some loop. As a general
- * rule they should only be created with string-literal names
- * like "start-of-section" or "last-paragraph".
- *
- * Bookmarks are a powerful tool to enable complicated behavior.
- * Consider double-checking that you need this tool if you are
- * reaching for it, as inappropriate use could lead to broken
- * HTML structure or unwanted processing overhead.
- *
- * @since 6.2.0
- *
- * @param string $name Identifies this particular bookmark.
- * @return bool Whether the bookmark was successfully created.
- */
- public function set_bookmark( $name ) {
- if ( null === $this->tag_name_starts_at ) {
- return false;
- }
-
- if ( ! array_key_exists( $name, $this->bookmarks ) && count( $this->bookmarks ) >= self::MAX_BOOKMARKS ) {
- _doing_it_wrong(
- __METHOD__,
- __( 'Too many bookmarks: cannot create any more.' ),
- '6.2.0'
- );
- return false;
- }
-
- $this->bookmarks[ $name ] = new WP_HTML_Span(
- $this->tag_name_starts_at - ( $this->is_closing_tag ? 2 : 1 ),
- $this->tag_ends_at
- );
-
- return true;
- }
-
-
- /**
- * Removes a bookmark that is no longer needed.
- *
- * Releasing a bookmark frees up the small
- * performance overhead it requires.
- *
- * @param string $name Name of the bookmark to remove.
- * @return bool Whether the bookmark already existed before removal.
- */
- public function release_bookmark( $name ) {
- if ( ! array_key_exists( $name, $this->bookmarks ) ) {
- return false;
- }
-
- unset( $this->bookmarks[ $name ] );
-
- return true;
- }
-
-
- /**
- * Skips contents of title and textarea tags.
- *
- * @since 6.2.0
- *
- * @see https://html.spec.whatwg.org/multipage/parsing.html#rcdata-state
- *
- * @param string $tag_name The lowercase tag name which will close the RCDATA region.
- * @return bool Whether an end to the RCDATA region was found before the end of the document.
- */
- private function skip_rcdata( $tag_name ) {
- $html = $this->html;
- $doc_length = strlen( $html );
- $tag_length = strlen( $tag_name );
-
- $at = $this->bytes_already_parsed;
-
- while ( false !== $at && $at < $doc_length ) {
- $at = strpos( $this->html, '', $at );
-
- // If there is no possible tag closer then fail.
- if ( false === $at || ( $at + $tag_length ) >= $doc_length ) {
- $this->bytes_already_parsed = $doc_length;
- return false;
- }
-
- $closer_potentially_starts_at = $at;
- $at += 2;
-
- /*
- * Find a case-insensitive match to the tag name.
- *
- * Because tag names are limited to US-ASCII there is no
- * need to perform any kind of Unicode normalization when
- * comparing; any character which could be impacted by such
- * normalization could not be part of a tag name.
- */
- for ( $i = 0; $i < $tag_length; $i++ ) {
- $tag_char = $tag_name[ $i ];
- $html_char = $html[ $at + $i ];
-
- if ( $html_char !== $tag_char && strtoupper( $html_char ) !== $tag_char ) {
- $at += $i;
- continue 2;
- }
- }
-
- $at += $tag_length;
- $this->bytes_already_parsed = $at;
-
- /*
- * Ensure that the tag name terminates to avoid matching on
- * substrings of a longer tag name. For example, the sequence
- * "' !== $c ) {
- continue;
- }
-
- while ( $this->parse_next_attribute() ) {
- continue;
- }
- $at = $this->bytes_already_parsed;
- if ( $at >= strlen( $this->html ) ) {
- return false;
- }
-
- if ( '>' === $html[ $at ] || '/' === $html[ $at ] ) {
- $this->bytes_already_parsed = $closer_potentially_starts_at;
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Skips contents of script tags.
- *
- * @since 6.2.0
- *
- * @return bool Whether the script tag was closed before the end of the document.
- */
- private function skip_script_data() {
- $state = 'unescaped';
- $html = $this->html;
- $doc_length = strlen( $html );
- $at = $this->bytes_already_parsed;
-
- while ( false !== $at && $at < $doc_length ) {
- $at += strcspn( $html, '-<', $at );
-
- /*
- * For all script states a "-->" transitions
- * back into the normal unescaped script mode,
- * even if that's the current state.
- */
- if (
- $at + 2 < $doc_length &&
- '-' === $html[ $at ] &&
- '-' === $html[ $at + 1 ] &&
- '>' === $html[ $at + 2 ]
- ) {
- $at += 3;
- $state = 'unescaped';
- continue;
- }
-
- // Everything of interest past here starts with "<".
- if ( $at + 1 >= $doc_length || '<' !== $html[ $at++ ] ) {
- continue;
- }
-
- /*
- * Unlike with "-->", the "
- * https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
- */
- if (
- strlen( $html ) > $at + 3 &&
- '-' === $html[ $at + 2 ] &&
- '-' === $html[ $at + 3 ]
- ) {
- $closer_at = $at + 4;
- // If it's not possible to close the comment then there is nothing more to scan.
- if ( strlen( $html ) <= $closer_at ) {
- return false;
- }
-
- // Abruptly-closed empty comments are a sequence of dashes followed by `>`.
- $span_of_dashes = strspn( $html, '-', $closer_at );
- if ( '>' === $html[ $closer_at + $span_of_dashes ] ) {
- $at = $closer_at + $span_of_dashes + 1;
- continue;
- }
-
- /*
- * Comments may be closed by either a --> or an invalid --!>.
- * The first occurrence closes the comment.
- *
- * See https://html.spec.whatwg.org/#parse-error-incorrectly-closed-comment
- */
- $closer_at--; // Pre-increment inside condition below reduces risk of accidental infinite looping.
- while ( ++$closer_at < strlen( $html ) ) {
- $closer_at = strpos( $html, '--', $closer_at );
- if ( false === $closer_at ) {
- return false;
- }
-
- if ( $closer_at + 2 < strlen( $html ) && '>' === $html[ $closer_at + 2 ] ) {
- $at = $closer_at + 3;
- continue 2;
- }
-
- if ( $closer_at + 3 < strlen( $html ) && '!' === $html[ $closer_at + 2 ] && '>' === $html[ $closer_at + 3 ] ) {
- $at = $closer_at + 4;
- continue 2;
- }
- }
- }
-
- /*
- *
- * The CDATA is case-sensitive.
- * https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
- */
- if (
- strlen( $html ) > $at + 8 &&
- '[' === $html[ $at + 2 ] &&
- 'C' === $html[ $at + 3 ] &&
- 'D' === $html[ $at + 4 ] &&
- 'A' === $html[ $at + 5 ] &&
- 'T' === $html[ $at + 6 ] &&
- 'A' === $html[ $at + 7 ] &&
- '[' === $html[ $at + 8 ]
- ) {
- $closer_at = strpos( $html, ']]>', $at + 9 );
- if ( false === $closer_at ) {
- return false;
- }
-
- $at = $closer_at + 3;
- continue;
- }
-
- /*
- *
- * These are ASCII-case-insensitive.
- * https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
- */
- if (
- strlen( $html ) > $at + 8 &&
- ( 'D' === $html[ $at + 2 ] || 'd' === $html[ $at + 2 ] ) &&
- ( 'O' === $html[ $at + 3 ] || 'o' === $html[ $at + 3 ] ) &&
- ( 'C' === $html[ $at + 4 ] || 'c' === $html[ $at + 4 ] ) &&
- ( 'T' === $html[ $at + 5 ] || 't' === $html[ $at + 5 ] ) &&
- ( 'Y' === $html[ $at + 6 ] || 'y' === $html[ $at + 6 ] ) &&
- ( 'P' === $html[ $at + 7 ] || 'p' === $html[ $at + 7 ] ) &&
- ( 'E' === $html[ $at + 8 ] || 'e' === $html[ $at + 8 ] )
- ) {
- $closer_at = strpos( $html, '>', $at + 9 );
- if ( false === $closer_at ) {
- return false;
- }
-
- $at = $closer_at + 1;
- continue;
- }
-
- /*
- * Anything else here is an incorrectly-opened comment and transitions
- * to the bogus comment state - skip to the nearest >.
- */
- $at = strpos( $html, '>', $at + 1 );
- continue;
- }
-
- /*
- * > is a missing end tag name, which is ignored.
- *
- * See https://html.spec.whatwg.org/#parse-error-missing-end-tag-name
- */
- if ( '>' === $html[ $at + 1 ] ) {
- $at++;
- continue;
- }
-
- /*
- * transitions to a bogus comment state ā skip to the nearest >
- * See https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
- */
- if ( '?' === $html[ $at + 1 ] ) {
- $closer_at = strpos( $html, '>', $at + 2 );
- if ( false === $closer_at ) {
- return false;
- }
-
- $at = $closer_at + 1;
- continue;
- }
-
- /*
- * If a non-alpha starts the tag name in a tag closer it's a comment.
- * Find the first `>`, which closes the comment.
- *
- * See https://html.spec.whatwg.org/#parse-error-invalid-first-character-of-tag-name
- */
- if ( $this->is_closing_tag ) {
- $closer_at = strpos( $html, '>', $at + 3 );
- if ( false === $closer_at ) {
- return false;
- }
-
- $at = $closer_at + 1;
- continue;
- }
-
- ++$at;
- }
-
- return false;
- }
-
- /**
- * Parses the next attribute.
- *
- * @since 6.2.0
- *
- * @return bool Whether an attribute was found before the end of the document.
- */
- private function parse_next_attribute() {
- // Skip whitespace and slashes.
- $this->bytes_already_parsed += strspn( $this->html, " \t\f\r\n/", $this->bytes_already_parsed );
- if ( $this->bytes_already_parsed >= strlen( $this->html ) ) {
- return false;
- }
-
- /*
- * Treat the equal sign as a part of the attribute
- * name if it is the first encountered byte.
- *
- * @see https://html.spec.whatwg.org/multipage/parsing.html#before-attribute-name-state
- */
- $name_length = '=' === $this->html[ $this->bytes_already_parsed ]
- ? 1 + strcspn( $this->html, "=/> \t\f\r\n", $this->bytes_already_parsed + 1 )
- : strcspn( $this->html, "=/> \t\f\r\n", $this->bytes_already_parsed );
-
- // No attribute, just tag closer.
- if ( 0 === $name_length || $this->bytes_already_parsed + $name_length >= strlen( $this->html ) ) {
- return false;
- }
-
- $attribute_start = $this->bytes_already_parsed;
- $attribute_name = substr( $this->html, $attribute_start, $name_length );
- $this->bytes_already_parsed += $name_length;
- if ( $this->bytes_already_parsed >= strlen( $this->html ) ) {
- return false;
- }
-
- $this->skip_whitespace();
- if ( $this->bytes_already_parsed >= strlen( $this->html ) ) {
- return false;
- }
-
- $has_value = '=' === $this->html[ $this->bytes_already_parsed ];
- if ( $has_value ) {
- ++$this->bytes_already_parsed;
- $this->skip_whitespace();
- if ( $this->bytes_already_parsed >= strlen( $this->html ) ) {
- return false;
- }
-
- switch ( $this->html[ $this->bytes_already_parsed ] ) {
- case "'":
- case '"':
- $quote = $this->html[ $this->bytes_already_parsed ];
- $value_start = $this->bytes_already_parsed + 1;
- $value_length = strcspn( $this->html, $quote, $value_start );
- $attribute_end = $value_start + $value_length + 1;
- $this->bytes_already_parsed = $attribute_end;
- break;
-
- default:
- $value_start = $this->bytes_already_parsed;
- $value_length = strcspn( $this->html, "> \t\f\r\n", $value_start );
- $attribute_end = $value_start + $value_length;
- $this->bytes_already_parsed = $attribute_end;
- }
- } else {
- $value_start = $this->bytes_already_parsed;
- $value_length = 0;
- $attribute_end = $attribute_start + $name_length;
- }
-
- if ( $attribute_end >= strlen( $this->html ) ) {
- return false;
- }
-
- if ( $this->is_closing_tag ) {
- return true;
- }
-
- /*
- * > There must never be two or more attributes on
- * > the same start tag whose names are an ASCII
- * > case-insensitive match for each other.
- * - HTML 5 spec
- *
- * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive
- */
- $comparable_name = strtolower( $attribute_name );
-
- // If an attribute is listed many times, only use the first declaration and ignore the rest.
- if ( ! array_key_exists( $comparable_name, $this->attributes ) ) {
- $this->attributes[ $comparable_name ] = new WP_HTML_Attribute_Token(
- $attribute_name,
- $value_start,
- $value_length,
- $attribute_start,
- $attribute_end,
- ! $has_value
- );
- }
-
- return true;
- }
-
- /**
- * Move the internal cursor past any immediate successive whitespace.
- *
- * @since 6.2.0
- *
- * @return void
- */
- private function skip_whitespace() {
- $this->bytes_already_parsed += strspn( $this->html, " \t\f\r\n", $this->bytes_already_parsed );
- }
-
- /**
- * Applies attribute updates and cleans up once a tag is fully parsed.
- *
- * @since 6.2.0
- *
- * @return void
- */
- private function after_tag() {
- $this->get_updated_html();
- $this->tag_name_starts_at = null;
- $this->tag_name_length = null;
- $this->tag_ends_at = null;
- $this->is_closing_tag = null;
- $this->attributes = array();
- }
-
- /**
- * Converts class name updates into tag attributes updates
- * (they are accumulated in different data formats for performance).
- *
- * @since 6.2.0
- *
- * @see WP_HTML_Tag_Processor::$lexical_updates
- * @see WP_HTML_Tag_Processor::$classname_updates
- *
- * @return void
- */
- private function class_name_updates_to_attributes_updates() {
- if ( count( $this->classname_updates ) === 0 ) {
- return;
- }
-
- $existing_class = $this->get_enqueued_attribute_value( 'class' );
- if ( null === $existing_class || true === $existing_class ) {
- $existing_class = '';
- }
-
- if ( false === $existing_class && isset( $this->attributes['class'] ) ) {
- $existing_class = substr(
- $this->html,
- $this->attributes['class']->value_starts_at,
- $this->attributes['class']->value_length
- );
- }
-
- if ( false === $existing_class ) {
- $existing_class = '';
- }
-
- /**
- * Updated "class" attribute value.
- *
- * This is incrementally built while scanning through the existing class
- * attribute, skipping removed classes on the way, and then appending
- * added classes at the end. Only when finished processing will the
- * value contain the final new value.
-
- * @var string $class
- */
- $class = '';
-
- /**
- * Tracks the cursor position in the existing
- * class attribute value while parsing.
- *
- * @var int $at
- */
- $at = 0;
-
- /**
- * Indicates if there's any need to modify the existing class attribute.
- *
- * If a call to `add_class()` and `remove_class()` wouldn't impact
- * the `class` attribute value then there's no need to rebuild it.
- * For example, when adding a class that's already present or
- * removing one that isn't.
- *
- * This flag enables a performance optimization when none of the enqueued
- * class updates would impact the `class` attribute; namely, that the
- * processor can continue without modifying the input document, as if
- * none of the `add_class()` or `remove_class()` calls had been made.
- *
- * This flag is set upon the first change that requires a string update.
- *
- * @var bool $modified
- */
- $modified = false;
-
- // Remove unwanted classes by only copying the new ones.
- $existing_class_length = strlen( $existing_class );
- while ( $at < $existing_class_length ) {
- // Skip to the first non-whitespace character.
- $ws_at = $at;
- $ws_length = strspn( $existing_class, " \t\f\r\n", $ws_at );
- $at += $ws_length;
-
- // Capture the class name ā it's everything until the next whitespace.
- $name_length = strcspn( $existing_class, " \t\f\r\n", $at );
- if ( 0 === $name_length ) {
- // If no more class names are found then that's the end.
- break;
- }
-
- $name = substr( $existing_class, $at, $name_length );
- $at += $name_length;
-
- // If this class is marked for removal, start processing the next one.
- $remove_class = (
- isset( $this->classname_updates[ $name ] ) &&
- self::REMOVE_CLASS === $this->classname_updates[ $name ]
- );
-
- // If a class has already been seen then skip it; it should not be added twice.
- if ( ! $remove_class ) {
- $this->classname_updates[ $name ] = self::SKIP_CLASS;
- }
-
- if ( $remove_class ) {
- $modified = true;
- continue;
- }
-
- /*
- * Otherwise, append it to the new "class" attribute value.
- *
- * There are options for handling whitespace between tags.
- * Preserving the existing whitespace produces fewer changes
- * to the HTML content and should clarify the before/after
- * content when debugging the modified output.
- *
- * This approach contrasts normalizing the inter-class
- * whitespace to a single space, which might appear cleaner
- * in the output HTML but produce a noisier change.
- */
- $class .= substr( $existing_class, $ws_at, $ws_length );
- $class .= $name;
- }
-
- // Add new classes by appending those which haven't already been seen.
- foreach ( $this->classname_updates as $name => $operation ) {
- if ( self::ADD_CLASS === $operation ) {
- $modified = true;
-
- $class .= strlen( $class ) > 0 ? ' ' : '';
- $class .= $name;
- }
- }
-
- $this->classname_updates = array();
- if ( ! $modified ) {
- return;
- }
-
- if ( strlen( $class ) > 0 ) {
- $this->set_attribute( 'class', $class );
- } else {
- $this->remove_attribute( 'class' );
- }
- }
-
- /**
- * Applies attribute updates to HTML document.
- *
- * @since 6.2.0
- * @since 6.2.1 Accumulates shift for internal cursor and passed pointer.
- *
- * @param int $shift_this_point Accumulate and return shift for this position.
- * @return int How many bytes the given pointer moved in response to the updates.
- */
- private function apply_attributes_updates( $shift_this_point = 0 ) {
- if ( ! count( $this->lexical_updates ) ) {
- return 0;
- }
-
- $accumulated_shift_for_given_point = 0;
-
- /*
- * Attribute updates can be enqueued in any order but updates
- * to the document must occur in lexical order; that is, each
- * replacement must be made before all others which follow it
- * at later string indices in the input document.
- *
- * Sorting avoid making out-of-order replacements which
- * can lead to mangled output, partially-duplicated
- * attributes, and overwritten attributes.
- */
- usort( $this->lexical_updates, array( self::class, 'sort_start_ascending' ) );
-
- $bytes_already_copied = 0;
- $output_buffer = '';
- foreach ( $this->lexical_updates as $diff ) {
- $shift = strlen( $diff->text ) - ( $diff->end - $diff->start );
-
- // Adjust the cursor position by however much an update affects it.
- if ( $diff->start <= $this->bytes_already_parsed ) {
- $this->bytes_already_parsed += $shift;
- }
-
- // Accumulate shift of the given pointer within this function call.
- if ( $diff->start <= $shift_this_point ) {
- $accumulated_shift_for_given_point += $shift;
- }
-
- $output_buffer .= substr( $this->html, $bytes_already_copied, $diff->start - $bytes_already_copied );
- $output_buffer .= $diff->text;
- $bytes_already_copied = $diff->end;
- }
-
- $this->html = $output_buffer . substr( $this->html, $bytes_already_copied );
-
- /*
- * Adjust bookmark locations to account for how the text
- * replacements adjust offsets in the input document.
- */
- foreach ( $this->bookmarks as $bookmark ) {
- /*
- * Each lexical update which appears before the bookmark's endpoints
- * might shift the offsets for those endpoints. Loop through each change
- * and accumulate the total shift for each bookmark, then apply that
- * shift after tallying the full delta.
- */
- $head_delta = 0;
- $tail_delta = 0;
-
- foreach ( $this->lexical_updates as $diff ) {
- $update_head = $bookmark->start >= $diff->start;
- $update_tail = $bookmark->end >= $diff->start;
-
- if ( ! $update_head && ! $update_tail ) {
- break;
- }
-
- $delta = strlen( $diff->text ) - ( $diff->end - $diff->start );
-
- if ( $update_head ) {
- $head_delta += $delta;
- }
-
- if ( $update_tail ) {
- $tail_delta += $delta;
- }
- }
-
- $bookmark->start += $head_delta;
- $bookmark->end += $tail_delta;
- }
-
- $this->lexical_updates = array();
-
- return $accumulated_shift_for_given_point;
- }
-
- /**
- * Move the internal cursor in the Tag Processor to a given bookmark's location.
- *
- * In order to prevent accidental infinite loops, there's a
- * maximum limit on the number of times seek() can be called.
- *
- * @since 6.2.0
- *
- * @param string $bookmark_name Jump to the place in the document identified by this bookmark name.
- * @return bool Whether the internal cursor was successfully moved to the bookmark's location.
- */
- public function seek( $bookmark_name ) {
- if ( ! array_key_exists( $bookmark_name, $this->bookmarks ) ) {
- _doing_it_wrong(
- __METHOD__,
- __( 'Unknown bookmark name.' ),
- '6.2.0'
- );
- return false;
- }
-
- if ( ++$this->seek_count > self::MAX_SEEK_OPS ) {
- _doing_it_wrong(
- __METHOD__,
- __( 'Too many calls to seek() - this can lead to performance issues.' ),
- '6.2.0'
- );
- return false;
- }
-
- // Flush out any pending updates to the document.
- $this->get_updated_html();
-
- // Point this tag processor before the sought tag opener and consume it.
- $this->bytes_already_parsed = $this->bookmarks[ $bookmark_name ]->start;
- return $this->next_tag( array( 'tag_closers' => 'visit' ) );
- }
-
- /**
- * Compare two WP_HTML_Text_Replacement objects.
- *
- * @since 6.2.0
- *
- * @param WP_HTML_Text_Replacement $a First attribute update.
- * @param WP_HTML_Text_Replacement $b Second attribute update.
- * @return int Comparison value for string order.
- */
- private static function sort_start_ascending( $a, $b ) {
- $by_start = $a->start - $b->start;
- if ( 0 !== $by_start ) {
- return $by_start;
- }
-
- $by_text = isset( $a->text, $b->text ) ? strcmp( $a->text, $b->text ) : 0;
- if ( 0 !== $by_text ) {
- return $by_text;
- }
-
- /*
- * This code should be unreachable, because it implies the two replacements
- * start at the same location and contain the same text.
- */
- return $a->end - $b->end;
- }
-
- /**
- * Return the enqueued value for a given attribute, if one exists.
- *
- * Enqueued updates can take different data types:
- * - If an update is enqueued and is boolean, the return will be `true`
- * - If an update is otherwise enqueued, the return will be the string value of that update.
- * - If an attribute is enqueued to be removed, the return will be `null` to indicate that.
- * - If no updates are enqueued, the return will be `false` to differentiate from "removed."
- *
- * @since 6.2.0
- *
- * @param string $comparable_name The attribute name in its comparable form.
- * @return string|boolean|null Value of enqueued update if present, otherwise false.
- */
- private function get_enqueued_attribute_value( $comparable_name ) {
- if ( ! isset( $this->lexical_updates[ $comparable_name ] ) ) {
- return false;
- }
-
- $enqueued_text = $this->lexical_updates[ $comparable_name ]->text;
-
- // Removed attributes erase the entire span.
- if ( '' === $enqueued_text ) {
- return null;
- }
-
- /*
- * Boolean attribute updates are just the attribute name without a corresponding value.
- *
- * This value might differ from the given comparable name in that there could be leading
- * or trailing whitespace, and that the casing follows the name given in `set_attribute`.
- *
- * Example:
- *
- * $p->set_attribute( 'data-TEST-id', 'update' );
- * 'update' === $p->get_enqueued_attribute_value( 'data-test-id' );
- *
- * Detect this difference based on the absence of the `=`, which _must_ exist in any
- * attribute containing a value, e.g. ``.
- * Ā¹ Ā²
- * 1. Attribute with a string value.
- * 2. Boolean attribute whose value is `true`.
- */
- $equals_at = strpos( $enqueued_text, '=' );
- if ( false === $equals_at ) {
- return true;
- }
-
- /*
- * Finally, a normal update's value will appear after the `=` and
- * be double-quoted, as performed incidentally by `set_attribute`.
- *
- * e.g. `type="text"`
- * Ā¹Ā² Ā³
- * 1. Equals is here.
- * 2. Double-quoting starts one after the equals sign.
- * 3. Double-quoting ends at the last character in the update.
- */
- $enqueued_value = substr( $enqueued_text, $equals_at + 2, -1 );
- return html_entity_decode( $enqueued_value );
- }
-
- /**
- * Returns the value of a requested attribute from a matched tag opener if that attribute exists.
- *
- * Example:
- *
- * $p = new WP_HTML_Tag_Processor( '
Test
' );
- * $p->next_tag( array( 'class_name' => 'test' ) ) === true;
- * $p->get_attribute( 'data-test-id' ) === '14';
- * $p->get_attribute( 'enabled' ) === true;
- * $p->get_attribute( 'aria-label' ) === null;
- *
- * $p->next_tag() === false;
- * $p->get_attribute( 'class' ) === null;
- *
- * @since 6.2.0
- *
- * @param string $name Name of attribute whose value is requested.
- * @return string|true|null Value of attribute or `null` if not available. Boolean attributes return `true`.
- */
- public function get_attribute( $name ) {
- if ( null === $this->tag_name_starts_at ) {
- return null;
- }
-
- $comparable = strtolower( $name );
-
- /*
- * For every attribute other than `class` it's possible to perform a quick check if
- * there's an enqueued lexical update whose value takes priority over what's found in
- * the input document.
- *
- * The `class` attribute is special though because of the exposed helpers `add_class`
- * and `remove_class`. These form a builder for the `class` attribute, so an additional
- * check for enqueued class changes is required in addition to the check for any enqueued
- * attribute values. If any exist, those enqueued class changes must first be flushed out
- * into an attribute value update.
- */
- if ( 'class' === $name ) {
- $this->class_name_updates_to_attributes_updates();
- }
-
- // Return any enqueued attribute value updates if they exist.
- $enqueued_value = $this->get_enqueued_attribute_value( $comparable );
- if ( false !== $enqueued_value ) {
- return $enqueued_value;
- }
-
- if ( ! isset( $this->attributes[ $comparable ] ) ) {
- return null;
- }
-
- $attribute = $this->attributes[ $comparable ];
-
- /*
- * This flag distinguishes an attribute with no value
- * from an attribute with an empty string value. For
- * unquoted attributes this could look very similar.
- * It refers to whether an `=` follows the name.
- *
- * e.g.
- * Ā¹ Ā²
- * 1. Attribute `boolean-attribute` is `true`.
- * 2. Attribute `empty-attribute` is `""`.
- */
- if ( true === $attribute->is_true ) {
- return true;
- }
-
- $raw_value = substr( $this->html, $attribute->value_starts_at, $attribute->value_length );
-
- return html_entity_decode( $raw_value );
- }
-
- /**
- * Gets lowercase names of all attributes matching a given prefix in the current tag.
- *
- * Note that matching is case-insensitive. This is in accordance with the spec:
- *
- * > There must never be two or more attributes on
- * > the same start tag whose names are an ASCII
- * > case-insensitive match for each other.
- * - HTML 5 spec
- *
- * Example:
- *
- * $p = new WP_HTML_Tag_Processor( '
' );
- * $p->next_tag() === true;
- * $p->get_tag() === 'DIV';
- *
- * $p->next_tag() === false;
- * $p->get_tag() === null;
- *
- * @since 6.2.0
- *
- * @return string|null Name of currently matched tag in input HTML, or `null` if none found.
- */
- public function get_tag() {
- if ( null === $this->tag_name_starts_at ) {
- return null;
- }
-
- $tag_name = substr( $this->html, $this->tag_name_starts_at, $this->tag_name_length );
-
- return strtoupper( $tag_name );
- }
-
- /**
- * Indicates if the current tag token is a tag closer.
- *
- * Example:
- *
- * $p = new WP_HTML_Tag_Processor( '' );
- * $p->next_tag( array( 'tag_name' => 'div', 'tag_closers' => 'visit' ) );
- * $p->is_tag_closer() === false;
- *
- * $p->next_tag( array( 'tag_name' => 'div', 'tag_closers' => 'visit' ) );
- * $p->is_tag_closer() === true;
- *
- * @since 6.2.0
- *
- * @return bool Whether the current tag is a tag closer.
- */
- public function is_tag_closer() {
- return $this->is_closing_tag;
- }
-
- /**
- * Updates or creates a new attribute on the currently matched tag with the passed value.
- *
- * For boolean attributes special handling is provided:
- * - When `true` is passed as the value, then only the attribute name is added to the tag.
- * - When `false` is passed, the attribute gets removed if it existed before.
- *
- * For string attributes, the value is escaped using the `esc_attr` function.
- *
- * @since 6.2.0
- * @since 6.2.1 Fix: Only create a single update for multiple calls with case-variant attribute names.
- *
- * @param string $name The attribute name to target.
- * @param string|bool $value The new attribute value.
- * @return bool Whether an attribute value was set.
- */
- public function set_attribute( $name, $value ) {
- if ( $this->is_closing_tag || null === $this->tag_name_starts_at ) {
- return false;
- }
-
- /*
- * WordPress rejects more characters than are strictly forbidden
- * in HTML5. This is to prevent additional security risks deeper
- * in the WordPress and plugin stack. Specifically the
- * less-than (<) greater-than (>) and ampersand (&) aren't allowed.
- *
- * The use of a PCRE match enables looking for specific Unicode
- * code points without writing a UTF-8 decoder. Whereas scanning
- * for one-byte characters is trivial (with `strcspn`), scanning
- * for the longer byte sequences would be more complicated. Given
- * that this shouldn't be in the hot path for execution, it's a
- * reasonable compromise in efficiency without introducing a
- * noticeable impact on the overall system.
- *
- * @see https://html.spec.whatwg.org/#attributes-2
- *
- * @TODO as the only regex pattern maybe we should take it out? are
- * Unicode patterns available broadly in Core?
- */
- if ( preg_match(
- '~[' .
- // Syntax-like characters.
- '"\'>& =' .
- // Control characters.
- '\x{00}-\x{1F}' .
- // HTML noncharacters.
- '\x{FDD0}-\x{FDEF}' .
- '\x{FFFE}\x{FFFF}\x{1FFFE}\x{1FFFF}\x{2FFFE}\x{2FFFF}\x{3FFFE}\x{3FFFF}' .
- '\x{4FFFE}\x{4FFFF}\x{5FFFE}\x{5FFFF}\x{6FFFE}\x{6FFFF}\x{7FFFE}\x{7FFFF}' .
- '\x{8FFFE}\x{8FFFF}\x{9FFFE}\x{9FFFF}\x{AFFFE}\x{AFFFF}\x{BFFFE}\x{BFFFF}' .
- '\x{CFFFE}\x{CFFFF}\x{DFFFE}\x{DFFFF}\x{EFFFE}\x{EFFFF}\x{FFFFE}\x{FFFFF}' .
- '\x{10FFFE}\x{10FFFF}' .
- ']~Ssu',
- $name
- ) ) {
- _doing_it_wrong(
- __METHOD__,
- __( 'Invalid attribute name.' ),
- '6.2.0'
- );
-
- return false;
- }
-
- /*
- * > The values "true" and "false" are not allowed on boolean attributes.
- * > To represent a false value, the attribute has to be omitted altogether.
- * - HTML5 spec, https://html.spec.whatwg.org/#boolean-attributes
- */
- if ( false === $value ) {
- return $this->remove_attribute( $name );
- }
-
- if ( true === $value ) {
- $updated_attribute = $name;
- } else {
- $escaped_new_value = esc_attr( $value );
- $updated_attribute = "{$name}=\"{$escaped_new_value}\"";
- }
-
- /*
- * > There must never be two or more attributes on
- * > the same start tag whose names are an ASCII
- * > case-insensitive match for each other.
- * - HTML 5 spec
- *
- * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive
- */
- $comparable_name = strtolower( $name );
-
- if ( isset( $this->attributes[ $comparable_name ] ) ) {
- /*
- * Update an existing attribute.
- *
- * Example ā set attribute id to "new" in :
- *
- *
- * ^-------------^
- * start end
- * replacement: `id="new"`
- *
- * Result:
- */
- $existing_attribute = $this->attributes[ $comparable_name ];
- $this->lexical_updates[ $comparable_name ] = new WP_HTML_Text_Replacement(
- $existing_attribute->start,
- $existing_attribute->end,
- $updated_attribute
- );
- } else {
- /*
- * Create a new attribute at the tag's name end.
- *
- * Example ā add attribute id="new" to :
- *
- *
- * ^
- * start and end
- * replacement: ` id="new"`
- *
- * Result:
- */
- $this->lexical_updates[ $comparable_name ] = new WP_HTML_Text_Replacement(
- $this->tag_name_starts_at + $this->tag_name_length,
- $this->tag_name_starts_at + $this->tag_name_length,
- ' ' . $updated_attribute
- );
- }
-
- /*
- * Any calls to update the `class` attribute directly should wipe out any
- * enqueued class changes from `add_class` and `remove_class`.
- */
- if ( 'class' === $comparable_name && ! empty( $this->classname_updates ) ) {
- $this->classname_updates = array();
- }
-
- return true;
- }
-
- /**
- * Remove an attribute from the currently-matched tag.
- *
- * @since 6.2.0
- *
- * @param string $name The attribute name to remove.
- * @return bool Whether an attribute was removed.
- */
- public function remove_attribute( $name ) {
- if ( $this->is_closing_tag ) {
- return false;
- }
-
- /*
- * > There must never be two or more attributes on
- * > the same start tag whose names are an ASCII
- * > case-insensitive match for each other.
- * - HTML 5 spec
- *
- * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive
- */
- $name = strtolower( $name );
-
- /*
- * Any calls to update the `class` attribute directly should wipe out any
- * enqueued class changes from `add_class` and `remove_class`.
- */
- if ( 'class' === $name && count( $this->classname_updates ) !== 0 ) {
- $this->classname_updates = array();
- }
-
- /*
- * If updating an attribute that didn't exist in the input
- * document, then remove the enqueued update and move on.
- *
- * For example, this might occur when calling `remove_attribute()`
- * after calling `set_attribute()` for the same attribute
- * and when that attribute wasn't originally present.
- */
- if ( ! isset( $this->attributes[ $name ] ) ) {
- if ( isset( $this->lexical_updates[ $name ] ) ) {
- unset( $this->lexical_updates[ $name ] );
- }
- return false;
- }
-
- /*
- * Removes an existing tag attribute.
- *
- * Example ā remove the attribute id from :
- *
- * ^-------------^
- * start end
- * replacement: ``
- *
- * Result:
- */
- $this->lexical_updates[ $name ] = new WP_HTML_Text_Replacement(
- $this->attributes[ $name ]->start,
- $this->attributes[ $name ]->end,
- ''
- );
-
- return true;
- }
-
- /**
- * Adds a new class name to the currently matched tag.
- *
- * @since 6.2.0
- *
- * @param string $class_name The class name to add.
- * @return bool Whether the class was set to be added.
- */
- public function add_class( $class_name ) {
- if ( $this->is_closing_tag ) {
- return false;
- }
-
- if ( null !== $this->tag_name_starts_at ) {
- $this->classname_updates[ $class_name ] = self::ADD_CLASS;
- }
-
- return true;
- }
-
- /**
- * Removes a class name from the currently matched tag.
- *
- * @since 6.2.0
- *
- * @param string $class_name The class name to remove.
- * @return bool Whether the class was set to be removed.
- */
- public function remove_class( $class_name ) {
- if ( $this->is_closing_tag ) {
- return false;
- }
-
- if ( null !== $this->tag_name_starts_at ) {
- $this->classname_updates[ $class_name ] = self::REMOVE_CLASS;
- }
-
- return true;
- }
-
- /**
- * Returns the string representation of the HTML Tag Processor.
- *
- * @since 6.2.0
- *
- * @see WP_HTML_Tag_Processor::get_updated_html()
- *
- * @return string The processed HTML.
- */
- public function __toString() {
- return $this->get_updated_html();
- }
-
- /**
- * Returns the string representation of the HTML Tag Processor.
- *
- * @since 6.2.0
- * @since 6.2.1 Shifts the internal cursor corresponding to the applied updates.
- *
- * @return string The processed HTML.
- */
- public function get_updated_html() {
- $requires_no_updating = 0 === count( $this->classname_updates ) && 0 === count( $this->lexical_updates );
-
- /*
- * When there is nothing more to update and nothing has already been
- * updated, return the original document and avoid a string copy.
- */
- if ( $requires_no_updating ) {
- return $this->html;
- }
-
- /*
- * Keep track of the position right before the current tag. This will
- * be necessary for reparsing the current tag after updating the HTML.
- */
- $before_current_tag = $this->tag_name_starts_at - 1;
-
- /*
- * 1. Apply the enqueued edits and update all the pointers to reflect those changes.
- */
- $this->class_name_updates_to_attributes_updates();
- $before_current_tag += $this->apply_attributes_updates( $before_current_tag );
-
- /*
- * 2. Rewind to before the current tag and reparse to get updated attributes.
- *
- * At this point the internal cursor points to the end of the tag name.
- * Rewind before the tag name starts so that it's as if the cursor didn't
- * move; a call to `next_tag()` will reparse the recently-updated attributes
- * and additional calls to modify the attributes will apply at this same
- * location.
- *
- *
Previous HTMLMore HTML
- * ^ | back up by the length of the tag name plus the opening <
- * \<-/ back up by strlen("em") + 1 ==> 3
- */
-
- // Store existing state so it can be restored after reparsing.
- $previous_parsed_byte_count = $this->bytes_already_parsed;
- $previous_query = $this->last_query;
-
- // Reparse attributes.
- $this->bytes_already_parsed = $before_current_tag;
- $this->next_tag();
-
- // Restore previous state.
- $this->bytes_already_parsed = $previous_parsed_byte_count;
- $this->parse_query( $previous_query );
-
- return $this->html;
- }
-
- /**
- * Parses tag query input into internal search criteria.
- *
- * @since 6.2.0
- *
- * @param array|string|null $query {
- * Optional. Which tag name to find, having which class, etc. Default is to find any tag.
- *
- * @type string|null $tag_name Which tag to find, or `null` for "any tag."
- * @type int|null $match_offset Find the Nth tag matching all search criteria.
- * 1 for "first" tag, 3 for "third," etc.
- * Defaults to first tag.
- * @type string|null $class_name Tag must contain this class name to match.
- * @type string $tag_closers "visit" or "skip": whether to stop on tag closers, e.g.
.
- * }
- * @return void
- */
- private function parse_query( $query ) {
- if ( null !== $query && $query === $this->last_query ) {
- return;
- }
-
- $this->last_query = $query;
- $this->sought_tag_name = null;
- $this->sought_class_name = null;
- $this->sought_match_offset = 1;
- $this->stop_on_tag_closers = false;
-
- // A single string value means "find the tag of this name".
- if ( is_string( $query ) ) {
- $this->sought_tag_name = $query;
- return;
- }
-
- // An empty query parameter applies no restrictions on the search.
- if ( null === $query ) {
- return;
- }
-
- // If not using the string interface, an associative array is required.
- if ( ! is_array( $query ) ) {
- _doing_it_wrong(
- __METHOD__,
- __( 'The query argument must be an array or a tag name.' ),
- '6.2.0'
- );
- return;
- }
-
- if ( isset( $query['tag_name'] ) && is_string( $query['tag_name'] ) ) {
- $this->sought_tag_name = $query['tag_name'];
- }
-
- if ( isset( $query['class_name'] ) && is_string( $query['class_name'] ) ) {
- $this->sought_class_name = $query['class_name'];
- }
-
- if ( isset( $query['match_offset'] ) && is_int( $query['match_offset'] ) && 0 < $query['match_offset'] ) {
- $this->sought_match_offset = $query['match_offset'];
- }
-
- if ( isset( $query['tag_closers'] ) ) {
- $this->stop_on_tag_closers = 'visit' === $query['tag_closers'];
- }
- }
-
-
- /**
- * Checks whether a given tag and its attributes match the search criteria.
- *
- * @since 6.2.0
- *
- * @return boolean Whether the given tag and its attribute match the search criteria.
- */
- private function matches() {
- if ( $this->is_closing_tag && ! $this->stop_on_tag_closers ) {
- return false;
- }
-
- // Does the tag name match the requested tag name in a case-insensitive manner?
- if ( null !== $this->sought_tag_name ) {
- /*
- * String (byte) length lookup is fast. If they aren't the
- * same length then they can't be the same string values.
- */
- if ( strlen( $this->sought_tag_name ) !== $this->tag_name_length ) {
- return false;
- }
-
- /*
- * Check each character to determine if they are the same.
- * Defer calls to `strtoupper()` to avoid them when possible.
- * Calling `strcasecmp()` here tested slowed than comparing each
- * character, so unless benchmarks show otherwise, it should
- * not be used.
- *
- * It's expected that most of the time that this runs, a
- * lower-case tag name will be supplied and the input will
- * contain lower-case tag names, thus normally bypassing
- * the case comparison code.
- */
- for ( $i = 0; $i < $this->tag_name_length; $i++ ) {
- $html_char = $this->html[ $this->tag_name_starts_at + $i ];
- $tag_char = $this->sought_tag_name[ $i ];
-
- if ( $html_char !== $tag_char && strtoupper( $html_char ) !== $tag_char ) {
- return false;
- }
- }
- }
-
- $needs_class_name = null !== $this->sought_class_name;
-
- if ( $needs_class_name && ! isset( $this->attributes['class'] ) ) {
- return false;
- }
-
- /*
- * Match byte-for-byte (case-sensitive and encoding-form-sensitive) on the class name.
- *
- * This will overlook certain classes that exist in other lexical variations
- * than was supplied to the search query, but requires more complicated searching.
- */
- if ( $needs_class_name ) {
- $class_start = $this->attributes['class']->value_starts_at;
- $class_end = $class_start + $this->attributes['class']->value_length;
- $class_at = $class_start;
-
- /*
- * Ensure that boundaries surround the class name to avoid matching on
- * substrings of a longer name. For example, the sequence "not-odd"
- * should not match for the class "odd" even though "odd" is found
- * within the class attribute text.
- *
- * See https://html.spec.whatwg.org/#attributes-3
- * See https://html.spec.whatwg.org/#space-separated-tokens
- */
- while (
- // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
- false !== ( $class_at = strpos( $this->html, $this->sought_class_name, $class_at ) ) &&
- $class_at < $class_end
- ) {
- /*
- * Verify this class starts at a boundary.
- */
- if ( $class_at > $class_start ) {
- $character = $this->html[ $class_at - 1 ];
-
- if ( ' ' !== $character && "\t" !== $character && "\f" !== $character && "\r" !== $character && "\n" !== $character ) {
- $class_at += strlen( $this->sought_class_name );
- continue;
- }
- }
-
- /*
- * Verify this class ends at a boundary as well.
- */
- if ( $class_at + strlen( $this->sought_class_name ) < $class_end ) {
- $character = $this->html[ $class_at + strlen( $this->sought_class_name ) ];
-
- if ( ' ' !== $character && "\t" !== $character && "\f" !== $character && "\r" !== $character && "\n" !== $character ) {
- $class_at += strlen( $this->sought_class_name );
- continue;
- }
- }
-
- return true;
- }
-
- return false;
- }
-
- return true;
- }
-}
diff --git a/lib/compat/wordpress-6.2/html-api/class-wp-html-text-replacement.php b/lib/compat/wordpress-6.2/html-api/class-wp-html-text-replacement.php
deleted file mode 100644
index b3f70c8e7c57f..0000000000000
--- a/lib/compat/wordpress-6.2/html-api/class-wp-html-text-replacement.php
+++ /dev/null
@@ -1,63 +0,0 @@
-start = $start;
- $this->end = $end;
- $this->text = $text;
- }
-}
diff --git a/lib/compat/wordpress-6.2/menu.php b/lib/compat/wordpress-6.2/menu.php
deleted file mode 100644
index e0d582ad3dc98..0000000000000
--- a/lib/compat/wordpress-6.2/menu.php
+++ /dev/null
@@ -1,35 +0,0 @@
- $menu_item ) {
- if ( str_contains( $menu_item[2], 'site-editor.php?postType=wp_template_part' ) && ! str_contains( $menu_item[2], 'path=' ) ) {
- $submenu['themes.php'][ $index ][2] = 'site-editor.php?postType=wp_template_part&path=/wp_template_part/all';
- break;
- }
- }
-}
-add_action( 'admin_menu', 'gutenberg_update_template_parts_menu_url' );
diff --git a/lib/compat/wordpress-6.2/rest-api.php b/lib/compat/wordpress-6.2/rest-api.php
deleted file mode 100644
index 97f7daecdff2f..0000000000000
--- a/lib/compat/wordpress-6.2/rest-api.php
+++ /dev/null
@@ -1,145 +0,0 @@
-register_routes();
-}
-add_action( 'rest_api_init', 'gutenberg_register_rest_block_pattern_categories' );
-
-/**
- * Add extra collection params to pattern directory requests.
- *
- * @param array $query_params JSON Schema-formatted collection parameters.
- * @return array Updated parameters.
- */
-function gutenberg_pattern_directory_collection_params_6_2( $query_params ) {
- $query_params['page'] = array(
- 'description' => __( 'Current page of the collection.', 'gutenberg' ),
- 'type' => 'integer',
- 'default' => 1,
- 'sanitize_callback' => 'absint',
- 'validate_callback' => 'rest_validate_request_arg',
- 'minimum' => 1,
- );
-
- $query_params['per_page'] = array(
- 'description' => __( 'Maximum number of items to be returned in result set.', 'gutenberg' ),
- 'type' => 'integer',
- 'default' => 100,
- 'minimum' => 1,
- 'maximum' => 100,
- 'sanitize_callback' => 'absint',
- 'validate_callback' => 'rest_validate_request_arg',
- );
-
- $query_params['offset'] = array(
- 'description' => __( 'Offset the result set by a specific number of items.', 'gutenberg' ),
- 'type' => 'integer',
- );
-
- $query_params['order'] = array(
- 'description' => __( 'Order sort attribute ascending or descending.', 'gutenberg' ),
- 'type' => 'string',
- 'default' => 'desc',
- 'enum' => array( 'asc', 'desc' ),
- );
-
- $query_params['orderby'] = array(
- 'description' => __( 'Sort collection by post attribute.', 'gutenberg' ),
- 'type' => 'string',
- 'default' => 'date',
- 'enum' => array(
- 'author',
- 'date',
- 'id',
- 'include',
- 'modified',
- 'parent',
- 'relevance',
- 'slug',
- 'include_slugs',
- 'title',
- 'favorite_count',
- ),
- );
-
- return $query_params;
-}
-add_filter( 'rest_pattern_directory_collection_params', 'gutenberg_pattern_directory_collection_params_6_2' );
-
-/**
- * Updates REST API response for the sidebars and marks them as 'inactive'.
- *
- * Note: This can be a part of the `prepare_item_for_response` in `class-wp-rest-sidebars-controller.php`.
- *
- * @param WP_REST_Response $response The sidebar response object.
- * @return WP_REST_Response $response Updated response object.
- */
-function gutenberg_modify_rest_sidebars_response( $response ) {
- if ( wp_is_block_theme() ) {
- $response->data['status'] = 'inactive';
- }
- return $response;
-}
-add_filter( 'rest_prepare_sidebar', 'gutenberg_modify_rest_sidebars_response' );
-
-if ( ! function_exists( 'add_block_pattern_block_types_schema' ) ) {
- /**
- * Add the `block_types` value to the `pattern-directory-item` schema.
- *
- * @since 6.2.0 Added 'block_types' property.
- */
- function add_block_pattern_block_types_schema() {
- register_rest_field(
- 'pattern-directory-item',
- 'block_types',
- array(
- 'schema' => array(
- 'description' => __( 'The block types which can use this pattern.', 'gutenberg' ),
- 'type' => 'array',
- 'uniqueItems' => true,
- 'items' => array( 'type' => 'string' ),
- 'context' => array( 'view', 'embed' ),
- ),
- )
- );
- }
-}
-add_filter( 'rest_api_init', 'add_block_pattern_block_types_schema' );
-
-
-if ( ! function_exists( 'filter_block_pattern_response' ) ) {
- /**
- * Add the `block_types` value into the API response.
- *
- * @since 6.2.0 Added 'block_types' property.
- *
- * @param WP_REST_Response $response The response object.
- * @param object $raw_pattern The unprepared pattern.
- */
- function filter_block_pattern_response( $response, $raw_pattern ) {
- $data = $response->get_data();
- $data['block_types'] = array_map( 'sanitize_text_field', $raw_pattern->meta->wpop_block_types );
- $response->set_data( $data );
- return $response;
- }
-}
-add_filter( 'rest_prepare_block_pattern', 'filter_block_pattern_response', 10, 2 );
-
-
-/**
- * Registers the block pattern directory.
- */
-function gutenberg_register_rest_pattern_directory() {
- $pattern_directory_controller = new Gutenberg_REST_Pattern_Directory_Controller_6_2();
- $pattern_directory_controller->register_routes();
-}
-add_action( 'rest_api_init', 'gutenberg_register_rest_pattern_directory' );
diff --git a/lib/compat/wordpress-6.2/script-loader.php b/lib/compat/wordpress-6.2/script-loader.php
deleted file mode 100644
index 37c1ced3c8cfc..0000000000000
--- a/lib/compat/wordpress-6.2/script-loader.php
+++ /dev/null
@@ -1,75 +0,0 @@
-query( 'wp-inert-polyfill', 'registered' );
- if ( ! $script ) {
- $scripts->add( 'wp-inert-polyfill', gutenberg_url( 'build/vendors/inert-polyfill' . $extension ), array() );
- }
-
- $script = $scripts->query( 'wp-polyfill', 'registered' );
- $script->deps = array_merge( $script->deps, array( 'wp-inert-polyfill' ) );
-}
-add_action( 'wp_default_scripts', 'gutenberg_register_vendor_scripts_62' );
-
-/**
- * This function takes care of adding inline styles
- * in the proper place, depending on the theme in use.
- *
- * This method was added to core in 5.9.1, but with a single param ($style). The second param ($priority) was
- * added post 6.0, so the 6.1 release needs to have wp_enqueue_block_support_styles updated to include this param.
- *
- * For block themes, it's loaded in the head.
- * For classic ones, it's loaded in the body
- * because the wp_head action happens before
- * the render_block.
- *
- * @link https://core.trac.wordpress.org/ticket/53494.
- *
- * @deprecated 6.2 Block supports styles are now stored for enqueuing via the style engine API. See: packages/style-engine/README.md.
- *
- * @param string $style String containing the CSS styles to be added.
- * @param int $priority To set the priority for the add_action.
- */
-function gutenberg_enqueue_block_support_styles( $style, $priority = 10 ) {
- _deprecated_function( __FUNCTION__, '6.2' );
-
- $action_hook_name = 'wp_footer';
- if ( wp_is_block_theme() ) {
- $action_hook_name = 'wp_head';
- }
- add_action(
- $action_hook_name,
- static function () use ( $style ) {
- echo "\n";
- },
- $priority
- );
-}
-
-add_filter(
- 'block_editor_settings_all',
- static function( $settings ) {
- // We must override what core is passing now.
- $settings['__unstableIsBlockBasedTheme'] = wp_is_block_theme();
- return $settings;
- },
- 100
-);
diff --git a/lib/compat/wordpress-6.2/site-editor.php b/lib/compat/wordpress-6.2/site-editor.php
deleted file mode 100644
index b6246e49c6d11..0000000000000
--- a/lib/compat/wordpress-6.2/site-editor.php
+++ /dev/null
@@ -1,24 +0,0 @@
-post ) ) {
- return $settings;
- }
-
- unset( $settings['__unstableHomeTemplate'] );
-
- return $settings;
-}
-add_filter( 'block_editor_settings_all', 'gutenberg_site_editor_unset_homepage_setting', 10, 2 );
diff --git a/lib/compat/wordpress-6.2/theme.php b/lib/compat/wordpress-6.2/theme.php
deleted file mode 100644
index 3e7b31109745a..0000000000000
--- a/lib/compat/wordpress-6.2/theme.php
+++ /dev/null
@@ -1,23 +0,0 @@
-is_block_theme() ) {
- set_theme_mod( 'wp_classic_sidebars', $wp_registered_sidebars );
- }
-}
-add_action( 'switch_theme', 'gutenberg_set_classic_sidebars', 10, 2 );
diff --git a/lib/compat/wordpress-6.2/widgets.php b/lib/compat/wordpress-6.2/widgets.php
deleted file mode 100644
index ce37f1bd9a34d..0000000000000
--- a/lib/compat/wordpress-6.2/widgets.php
+++ /dev/null
@@ -1,35 +0,0 @@
-content );
$post_content_block = gutenberg_find_first_block( 'core/post-content', $template_blocks );
- if ( ! empty( $post_content_block['attrs'] ) ) {
+ if ( isset( $post_content_block['attrs'] ) ) {
$settings['postContentAttributes'] = $post_content_block['attrs'];
}
}
diff --git a/lib/compat/wordpress-6.3/block-patterns.php b/lib/compat/wordpress-6.3/block-patterns.php
index 0ffa12484bfd4..bf7ef632846c2 100644
--- a/lib/compat/wordpress-6.3/block-patterns.php
+++ b/lib/compat/wordpress-6.3/block-patterns.php
@@ -75,7 +75,7 @@ function gutenberg_load_remote_block_patterns( $deprecated = null ) {
foreach ( $patterns as $pattern ) {
$pattern['source'] = 'pattern-directory/core'; // Added in 6.3.0.
- $normalized_pattern = gutenberg_normalize_remote_pattern( $pattern );
+ $normalized_pattern = wp_normalize_remote_block_pattern( $pattern );
$pattern_name = 'core/' . sanitize_title( $normalized_pattern['title'] );
register_block_pattern( $pattern_name, (array) $normalized_pattern );
}
@@ -110,7 +110,7 @@ function gutenberg_load_remote_featured_patterns() {
$registry = WP_Block_Patterns_Registry::get_instance();
foreach ( $patterns as $pattern ) {
$pattern['source'] = 'pattern-directory/featured'; // Added in 6.3.0.
- $normalized_pattern = gutenberg_normalize_remote_pattern( $pattern );
+ $normalized_pattern = wp_normalize_remote_block_pattern( $pattern );
$pattern_name = sanitize_title( $normalized_pattern['title'] );
// Some patterns might be already registered as core patterns with the `core` prefix.
$is_registered = $registry->is_registered( $pattern_name ) || $registry->is_registered( "core/$pattern_name" );
@@ -154,7 +154,7 @@ function gutenberg_register_remote_theme_patterns() {
$patterns_registry = WP_Block_Patterns_Registry::get_instance();
foreach ( $patterns as $pattern ) {
$pattern['source'] = 'pattern-directory/theme'; // Added in 6.3.0.
- $normalized_pattern = gutenberg_normalize_remote_pattern( $pattern );
+ $normalized_pattern = wp_normalize_remote_block_pattern( $pattern );
$pattern_name = sanitize_title( $normalized_pattern['title'] );
// Some patterns might be already registered as core patterns with the `core` prefix.
$is_registered = $patterns_registry->is_registered( $pattern_name ) || $patterns_registry->is_registered( "core/$pattern_name" );
diff --git a/lib/compat/wordpress-6.3/blocks.php b/lib/compat/wordpress-6.3/blocks.php
index 962c9ee3e07ef..001416b42566f 100644
--- a/lib/compat/wordpress-6.3/blocks.php
+++ b/lib/compat/wordpress-6.3/blocks.php
@@ -104,7 +104,7 @@ function gutenberg_wp_block_register_post_meta() {
$post_type,
'wp_pattern_sync_status',
array(
- 'auth_callback' => function() {
+ 'auth_callback' => function () {
return current_user_can( 'edit_posts' );
},
'sanitize_callback' => 'sanitize_text_field',
diff --git a/lib/compat/wordpress-6.3/class-gutenberg-navigation-fallback.php b/lib/compat/wordpress-6.3/class-gutenberg-navigation-fallback.php
index fcf6e13b0954d..fcd70da61f57e 100644
--- a/lib/compat/wordpress-6.3/class-gutenberg-navigation-fallback.php
+++ b/lib/compat/wordpress-6.3/class-gutenberg-navigation-fallback.php
@@ -154,7 +154,7 @@ private static function get_fallback_classic_menu() {
private static function get_most_recently_created_nav_menu( $classic_nav_menus ) {
usort(
$classic_nav_menus,
- static function( $a, $b ) {
+ static function ( $a, $b ) {
return $b->term_id - $a->term_id;
}
);
diff --git a/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php b/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php
index ac40e6b842f52..0a5b026cded3b 100644
--- a/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php
+++ b/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php
@@ -13,7 +13,15 @@
*
* @see WP_REST_Controller
*/
-class Gutenberg_REST_Block_Patterns_Controller_6_3 extends Gutenberg_REST_Block_Patterns_Controller_6_2 {
+class Gutenberg_REST_Block_Patterns_Controller_6_3 extends WP_REST_Block_Patterns_Controller {
+ /**
+ * Defines whether remote patterns should be loaded.
+ *
+ * @since 6.0.0
+ * @var bool
+ */
+ private $remote_patterns_loaded;
+
/**
* Prepare a raw block pattern before it gets output in a REST API response.
*
@@ -158,4 +166,33 @@ public function get_item_schema() {
return $this->add_additional_fields_schema( $this->schema );
}
+
+ /**
+ * Retrieves all block patterns.
+ *
+ * @since 6.0.0
+ * @since 6.2.0 Added migration for old core pattern categories to the new ones.
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function get_items( $request ) {
+ if ( ! $this->remote_patterns_loaded ) {
+ // Load block patterns from w.org.
+ gutenberg_load_remote_block_patterns(); // Patterns with the `core` keyword.
+ gutenberg_load_remote_featured_patterns(); // Patterns in the `featured` category.
+ gutenberg_register_remote_theme_patterns(); // Patterns requested by current theme.
+
+ $this->remote_patterns_loaded = true;
+ }
+
+ $response = array();
+ $patterns = WP_Block_Patterns_Registry::get_instance()->get_all_registered();
+ foreach ( $patterns as $pattern ) {
+ $migrated_pattern = $this->migrate_pattern_categories( $pattern );
+ $prepared_pattern = $this->prepare_item_for_response( $migrated_pattern, $request );
+ $response[] = $this->prepare_response_for_collection( $prepared_pattern );
+ }
+ return rest_ensure_response( $response );
+ }
}
diff --git a/lib/compat/wordpress-6.3/html-api/class-gutenberg-html-tag-processor-6-3.php b/lib/compat/wordpress-6.3/html-api/class-gutenberg-html-tag-processor-6-3.php
index b869e2ef1e1c1..73cdb9342490c 100644
--- a/lib/compat/wordpress-6.3/html-api/class-gutenberg-html-tag-processor-6-3.php
+++ b/lib/compat/wordpress-6.3/html-api/class-gutenberg-html-tag-processor-6-3.php
@@ -947,7 +947,7 @@ private function parse_next_tag() {
if ( '/' === $this->html[ $at + 1 ] ) {
$this->is_closing_tag = true;
- $at++;
+ ++$at;
} else {
$this->is_closing_tag = false;
}
@@ -1016,7 +1016,7 @@ private function parse_next_tag() {
*
* See https://html.spec.whatwg.org/#parse-error-incorrectly-closed-comment
*/
- $closer_at--; // Pre-increment inside condition below reduces risk of accidental infinite looping.
+ --$closer_at; // Pre-increment inside condition below reduces risk of accidental infinite looping.
while ( ++$closer_at < strlen( $html ) ) {
$closer_at = strpos( $html, '--', $closer_at );
if ( false === $closer_at ) {
@@ -1097,7 +1097,7 @@ private function parse_next_tag() {
* See https://html.spec.whatwg.org/#parse-error-missing-end-tag-name
*/
if ( '>' === $html[ $at + 1 ] ) {
- $at++;
+ ++$at;
continue;
}
@@ -1739,7 +1739,7 @@ public function get_attribute( $name ) {
* @param string $prefix Prefix of requested attribute names.
* @return array|null List of attribute names, or `null` when no tag opener is matched.
*/
- function get_attribute_names_with_prefix( $prefix ) {
+ public function get_attribute_names_with_prefix( $prefix ) {
if ( $this->is_closing_tag || null === $this->tag_name_starts_at ) {
return null;
}
@@ -2282,11 +2282,9 @@ private function matches() {
* See https://html.spec.whatwg.org/#attributes-3
* See https://html.spec.whatwg.org/#space-separated-tokens
*/
- while (
- // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
- false !== ( $class_at = strpos( $this->html, $this->sought_class_name, $class_at ) ) &&
- $class_at < $class_end
- ) {
+ do {
+ $class_at = strpos( $this->html, $this->sought_class_name, $class_at );
+
/*
* Verify this class starts at a boundary.
*/
@@ -2312,7 +2310,7 @@ private function matches() {
}
return true;
- }
+ } while ( false !== $class_at && $class_at < $class_end );
return false;
}
diff --git a/lib/compat/wordpress-6.3/rest-api.php b/lib/compat/wordpress-6.3/rest-api.php
index 130da6ed774d8..fc0c295bdaaf8 100644
--- a/lib/compat/wordpress-6.3/rest-api.php
+++ b/lib/compat/wordpress-6.3/rest-api.php
@@ -70,9 +70,9 @@ function add_modified_wp_template_schema() {
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
- 'get_callback' => function( $object ) {
- if ( ! empty( $object['wp_id'] ) ) {
- $post = get_post( $object['wp_id'] );
+ 'get_callback' => function ( $template_object ) {
+ if ( ! empty( $template_object['wp_id'] ) ) {
+ $post = get_post( $template_object['wp_id'] );
if ( $post && isset( $post->post_modified ) ) {
return mysql_to_rfc3339( $post->post_modified );
}
@@ -85,19 +85,6 @@ function add_modified_wp_template_schema() {
}
add_filter( 'rest_api_init', 'add_modified_wp_template_schema' );
-// If the Auto-inserting Blocks experiment is enabled, we load the block patterns
-// controller in lib/experimental/rest-api.php instead.
-if ( ! gutenberg_is_experiment_enabled( 'gutenberg-auto-inserting-blocks' ) ) {
- /**
- * Registers the block patterns REST API routes.
- */
- function gutenberg_register_rest_block_patterns() {
- $block_patterns = new Gutenberg_REST_Block_Patterns_Controller_6_3();
- $block_patterns->register_routes();
- }
- add_action( 'rest_api_init', 'gutenberg_register_rest_block_patterns' );
-}
-
/**
* Registers the Navigation Fallbacks REST API routes.
*/
diff --git a/lib/compat/wordpress-6.3/script-loader.php b/lib/compat/wordpress-6.3/script-loader.php
index 8f7bda2a64811..89149f3a75314 100644
--- a/lib/compat/wordpress-6.3/script-loader.php
+++ b/lib/compat/wordpress-6.3/script-loader.php
@@ -100,7 +100,7 @@ function _gutenberg_get_iframed_editor_assets() {
add_filter(
'block_editor_settings_all',
- static function( $settings ) {
+ static function ( $settings ) {
// We must override what core is passing now.
$settings['__unstableResolvedAssets'] = _gutenberg_get_iframed_editor_assets();
return $settings;
diff --git a/lib/experimental/auto-inserting-blocks.php b/lib/compat/wordpress-6.4/block-hooks.php
similarity index 65%
rename from lib/experimental/auto-inserting-blocks.php
rename to lib/compat/wordpress-6.4/block-hooks.php
index f4a650e110bf9..6fd7d4a4af1b4 100644
--- a/lib/experimental/auto-inserting-blocks.php
+++ b/lib/compat/wordpress-6.4/block-hooks.php
@@ -1,110 +1,26 @@
0 ) {
- if ( ! is_string( $block['innerContent'][ $chunk_index ] ) ) {
- $anchor_block_index--;
- }
- $chunk_index++;
- }
- // Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`)
- // when rendering blocks, we also need to insert a value (`null`, to mark a block
- // location) into that array.
- array_splice( $block['innerContent'], $chunk_index, 0, array( null ) );
- }
- return $block;
- };
-}
-
-/**
- * Add auto-insertion information to a block type's controller.
- *
- * @param array $inserted_block_type The type of block to insert.
- * @param string $position The position relative to the anchor block.
- * Can be 'before', 'after', 'first_child', or 'last_child'.
- * @param string $anchor_block_type The auto-inserted block will be inserted next to instances of this block type.
- * @return callable A filter for the `rest_prepare_block_type` hook that adds an `auto_insert` field to the network response.
- */
-function gutenberg_add_auto_insert_field_to_block_type_controller( $inserted_block_type, $position, $anchor_block_type ) {
- return function( $response, $block_type ) use ( $inserted_block_type, $position, $anchor_block_type ) {
- if ( $block_type->name !== $inserted_block_type ) {
- return $response;
- }
-
- $data = $response->get_data();
- if ( ! isset( $data['auto_insert'] ) ) {
- $data['auto_insert'] = array();
- }
- $data['auto_insert'][ $anchor_block_type ] = $position;
- $response->set_data( $data );
- return $response;
- };
-}
-
-/**
- * Register blocks for auto-insertion, based on their block.json metadata.
+ * Register hooked blocks for automatic insertion, based on their block.json metadata.
*
* @param array $settings Array of determined settings for registering a block type.
* @param array $metadata Metadata provided for registering a block type.
* @return array Updated settings array.
*/
-function gutenberg_register_auto_inserted_blocks( $settings, $metadata ) {
- if ( ! isset( $metadata['__experimentalAutoInsert'] ) ) {
+function gutenberg_add_hooked_blocks( $settings, $metadata ) {
+ if ( ! isset( $metadata['blockHooks'] ) ) {
return $settings;
}
- $auto_insert = $metadata['__experimentalAutoInsert'];
+ $block_hooks = $metadata['blockHooks'];
/**
* Map the camelCased position string from block.json to the snake_cased block type position
- * used in the auto-inserting block registration function.
+ * used in the hooked block registration function.
*
* @var array
*/
@@ -116,12 +32,12 @@ function gutenberg_register_auto_inserted_blocks( $settings, $metadata ) {
);
$inserted_block_name = $metadata['name'];
- foreach ( $auto_insert as $anchor_block_name => $position ) {
- // Avoid infinite recursion (auto-inserting next to or into self).
+ foreach ( $block_hooks as $anchor_block_name => $position ) {
+ // Avoid infinite recursion (hooking to itself).
if ( $inserted_block_name === $anchor_block_name ) {
_doing_it_wrong(
__METHOD__,
- __( 'Cannot auto-insert block next to itself.', 'gutenberg' ),
+ __( 'Cannot hook block to itself.', 'gutenberg' ),
'6.4.0'
);
continue;
@@ -133,9 +49,9 @@ function gutenberg_register_auto_inserted_blocks( $settings, $metadata ) {
$mapped_position = $property_mappings[ $position ];
- gutenberg_register_auto_inserted_block( $inserted_block_name, $mapped_position, $anchor_block_name );
+ gutenberg_add_hooked_block( $inserted_block_name, $mapped_position, $anchor_block_name );
- $settings['auto_insert'][ $anchor_block_name ] = $mapped_position;
+ $settings['block_hooks'][ $anchor_block_name ] = $mapped_position;
}
// Copied from `get_block_editor_server_block_settings()`.
@@ -158,12 +74,12 @@ function gutenberg_register_auto_inserted_blocks( $settings, $metadata ) {
'example' => 'example',
'variations' => 'variations',
);
- // Add `auto_insert` to the list of fields to pick.
- $fields_to_pick['auto_insert'] = 'autoInsert';
+ // Add `block_hooks` to the list of fields to pick.
+ $fields_to_pick['block_hooks'] = 'blockHooks';
$exposed_settings = array_intersect_key( $settings, $fields_to_pick );
- // TODO: Make work for blocks registered via direct call to gutenberg_register_auto_inserted_block().
+ // TODO: Make work for blocks registered via direct call to gutenberg_add_hooked_block().
wp_add_inline_script(
'wp-blocks',
'wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( array( $inserted_block_name => $exposed_settings ) ) . ');'
@@ -171,49 +87,152 @@ function gutenberg_register_auto_inserted_blocks( $settings, $metadata ) {
return $settings;
}
-add_filter( 'block_type_metadata_settings', 'gutenberg_register_auto_inserted_blocks', 10, 2 );
+add_filter( 'block_type_metadata_settings', 'gutenberg_add_hooked_blocks', 10, 2 );
/**
- * Register block for auto-insertion into the frontend and REST API.
+ * Register a hooked block for automatic insertion into a given block hook.
*
- * Register a block for auto-insertion into the frontend and into the markup
+ * A block hook is specified by a block type and a relative position. The hooked block
+ * will be automatically inserted in the given position next to the "anchor" block
+ * whenever the latter is encountered. This applies both to the frontend and to the markup
* returned by the templates and patterns REST API endpoints.
*
- * This is currently done by filtering parsed blocks as obtained from a block template
- * template part, or pattern and injecting the auto-inserted block where applicable.
+ * This is currently done by filtering parsed blocks as obtained from a block template,
+ * template part, or pattern, and injecting the hooked block where applicable.
*
- * @todo In the long run, we'd likely want some sort of registry for auto-inserted blocks.
+ * @todo In the long run, we'd likely want some sort of registry for hooked blocks.
*
- * @param string $inserted_block The name of the block to insert.
- * @param string $position The desired position of the auto-inserted block, relative to its anchor block.
- * Can be 'before', 'after', 'first_child', or 'last_child'.
- * @param string $anchor_block The name of the block to insert the auto-inserted block next to.
+ * @param string $hooked_block The name of the block to insert.
+ * @param string $position The desired position of the hooked block, relative to its anchor block.
+ * Can be 'before', 'after', 'first_child', or 'last_child'.
+ * @param string $anchor_block The name of the block to insert the hooked block next to.
* @return void
*/
-function gutenberg_register_auto_inserted_block( $inserted_block, $position, $anchor_block ) {
- $inserted_block_array = array(
- 'blockName' => $inserted_block,
+function gutenberg_add_hooked_block( $hooked_block, $position, $anchor_block ) {
+ $hooked_block_array = array(
+ 'blockName' => $hooked_block,
'attrs' => array(),
'innerHTML' => '',
'innerContent' => array(),
'innerBlocks' => array(),
);
- $inserter = gutenberg_auto_insert_block( $inserted_block_array, $position, $anchor_block );
+ $inserter = gutenberg_insert_hooked_block( $hooked_block_array, $position, $anchor_block );
add_filter( 'gutenberg_serialize_block', $inserter, 10, 1 );
/*
* The block-types REST API controller uses objects of the `WP_Block_Type` class, which are
* in turn created upon block type registration. However, that class does not contain
- * an `auto_insert` property (and is not easily extensible), so we have to use a different
- * mechanism to communicate to the controller which blocks have been registered for
- * auto-insertion. We're doing so here (i.e. upon block registration), by adding a filter to
+ * a `block_hooks` property (and is not easily extensible), so we have to use a different
+ * mechanism to communicate to the controller which hooked blocks have been registered for
+ * automatic insertion. We're doing so here (i.e. upon block registration), by adding a filter to
* the controller's response.
*/
- $controller_extender = gutenberg_add_auto_insert_field_to_block_type_controller( $inserted_block, $position, $anchor_block );
+ $controller_extender = gutenberg_add_block_hooks_field_to_block_type_controller( $hooked_block, $position, $anchor_block );
add_filter( 'rest_prepare_block_type', $controller_extender, 10, 2 );
}
+/**
+ * Return a function that auto-inserts a block next to a given "anchor" block.
+ *
+ * This is a helper function used in the implementation of block hooks.
+ * It is not meant for public use.
+ *
+ * The auto-inserted block can be inserted before or after the anchor block,
+ * or as the first or last child of the anchor block.
+ *
+ * Note that the returned function mutates the automatically inserted block's
+ * designated parent block by inserting into the parent's `innerBlocks` array,
+ * and by updating the parent's `innerContent` array accordingly.
+ *
+ * @param array $inserted_block The block to insert.
+ * @param string $relative_position The position relative to the given block.
+ * Can be 'before', 'after', 'first_child', or 'last_child'.
+ * @param string $anchor_block_type The automatically inserted block will be inserted next to instances of this block type.
+ * @return callable A function that accepts a block's content and returns the content with the inserted block.
+ */
+function gutenberg_insert_hooked_block( $inserted_block, $relative_position, $anchor_block_type ) {
+ return function ( $block ) use ( $inserted_block, $relative_position, $anchor_block_type ) {
+ if ( $anchor_block_type === $block['blockName'] ) {
+ if ( 'first_child' === $relative_position ) {
+ array_unshift( $block['innerBlocks'], $inserted_block );
+ // Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`)
+ // when rendering blocks, we also need to prepend a value (`null`, to mark a block
+ // location) to that array after HTML content for the inner blocks wrapper.
+ $chunk_index = 0;
+ for ( $index = $chunk_index; $index < count( $block['innerContent'] ); $index++ ) {
+ if ( is_null( $block['innerContent'][ $index ] ) ) {
+ $chunk_index = $index;
+ break;
+ }
+ }
+ array_splice( $block['innerContent'], $chunk_index, 0, array( null ) );
+ } elseif ( 'last_child' === $relative_position ) {
+ array_push( $block['innerBlocks'], $inserted_block );
+ // Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`)
+ // when rendering blocks, we also need to correctly append a value (`null`, to mark a block
+ // location) to that array before the remaining HTML content for the inner blocks wrapper.
+ $chunk_index = count( $block['innerContent'] );
+ for ( $index = count( $block['innerContent'] ); $index > 0; $index-- ) {
+ if ( is_null( $block['innerContent'][ $index - 1 ] ) ) {
+ $chunk_index = $index;
+ break;
+ }
+ }
+ array_splice( $block['innerContent'], $chunk_index, 0, array( null ) );
+ }
+ return $block;
+ }
+
+ $anchor_block_index = array_search( $anchor_block_type, array_column( $block['innerBlocks'], 'blockName' ), true );
+ if ( false !== $anchor_block_index && ( 'after' === $relative_position || 'before' === $relative_position ) ) {
+ if ( 'after' === $relative_position ) {
+ ++$anchor_block_index;
+ }
+ array_splice( $block['innerBlocks'], $anchor_block_index, 0, array( $inserted_block ) );
+
+ // Find matching `innerContent` chunk index.
+ $chunk_index = 0;
+ while ( $anchor_block_index > 0 ) {
+ if ( ! is_string( $block['innerContent'][ $chunk_index ] ) ) {
+ --$anchor_block_index;
+ }
+ ++$chunk_index;
+ }
+ // Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`)
+ // when rendering blocks, we also need to insert a value (`null`, to mark a block
+ // location) into that array.
+ array_splice( $block['innerContent'], $chunk_index, 0, array( null ) );
+ }
+ return $block;
+ };
+}
+
+/**
+ * Add block hooks information to a block type's controller.
+ *
+ * @param array $inserted_block_type The type of block to insert.
+ * @param string $position The position relative to the anchor block.
+ * Can be 'before', 'after', 'first_child', or 'last_child'.
+ * @param string $anchor_block_type The hooked block will be inserted next to instances of this block type.
+ * @return callable A filter for the `rest_prepare_block_type` hook that adds a `block_hooks` field to the network response.
+ */
+function gutenberg_add_block_hooks_field_to_block_type_controller( $inserted_block_type, $position, $anchor_block_type ) {
+ return function ( $response, $block_type ) use ( $inserted_block_type, $position, $anchor_block_type ) {
+ if ( $block_type->name !== $inserted_block_type ) {
+ return $response;
+ }
+
+ $data = $response->get_data();
+ if ( ! isset( $data['block_hooks'] ) ) {
+ $data['block_hooks'] = array();
+ }
+ $data['block_hooks'][ $anchor_block_type ] = $position;
+ $response->set_data( $data );
+ return $response;
+ };
+}
+
/**
* Parse and reserialize block templates to allow running filters.
*
@@ -325,17 +344,17 @@ function gutenberg_serialize_blocks( $blocks ) {
}
/**
- * Register the `auto_insert` field for the block-types REST API controller.
+ * Register the `block_hooks` field for the block-types REST API controller.
*
* @return void
*/
-function gutenberg_register_auto_insert_rest_field() {
+function gutenberg_register_block_hooks_rest_field() {
register_rest_field(
'block-type',
- 'auto_insert',
+ 'block_hooks',
array(
'schema' => array(
- 'description' => __( 'Block types that may be automatically inserted near this block and the associated relative position where they are inserted.', 'gutenberg' ),
+ 'description' => __( 'This block is automatically inserted near any occurence of the block types used as keys of this map, into a relative position given by the corresponding value.', 'gutenberg' ),
'patternProperties' => array(
'^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$' => array(
'type' => 'string',
@@ -346,4 +365,4 @@ function gutenberg_register_auto_insert_rest_field() {
)
);
}
-add_action( 'rest_api_init', 'gutenberg_register_auto_insert_rest_field' );
+add_action( 'rest_api_init', 'gutenberg_register_block_hooks_rest_field' );
diff --git a/lib/compat/wordpress-6.4/block-patterns.php b/lib/compat/wordpress-6.4/block-patterns.php
index aa4c5ef378bc7..922dea910b47a 100644
--- a/lib/compat/wordpress-6.4/block-patterns.php
+++ b/lib/compat/wordpress-6.4/block-patterns.php
@@ -16,20 +16,20 @@
*/
function gutenberg_register_taxonomy_patterns() {
$args = array(
- array(
- 'public' => false,
- 'hierarchical' => false,
- 'labels' => array(
- 'name' => _x( 'Pattern Categories', 'taxonomy general name' ),
- 'singular_name' => _x( 'Pattern Category', 'taxonomy singular name' ),
- ),
- 'query_var' => false,
- 'rewrite' => false,
- 'show_ui' => false,
- '_builtin' => true,
- 'show_in_nav_menus' => false,
- 'show_in_rest' => true,
+ 'public' => true,
+ 'publicly_queryable' => false,
+ 'hierarchical' => false,
+ 'labels' => array(
+ 'name' => _x( 'Pattern Categories', 'taxonomy general name' ),
+ 'singular_name' => _x( 'Pattern Category', 'taxonomy singular name' ),
),
+ 'query_var' => false,
+ 'rewrite' => false,
+ 'show_ui' => true,
+ '_builtin' => true,
+ 'show_in_nav_menus' => false,
+ 'show_in_rest' => true,
+ 'show_admin_column' => true,
);
register_taxonomy( 'wp_pattern_category', array( 'wp_block' ), $args );
}
diff --git a/lib/experimental/class-gutenberg-rest-block-patterns-controller.php b/lib/compat/wordpress-6.4/class-gutenberg-rest-block-patterns-controller.php
similarity index 93%
rename from lib/experimental/class-gutenberg-rest-block-patterns-controller.php
rename to lib/compat/wordpress-6.4/class-gutenberg-rest-block-patterns-controller.php
index 1ac567959b146..8128934b9b011 100644
--- a/lib/experimental/class-gutenberg-rest-block-patterns-controller.php
+++ b/lib/compat/wordpress-6.4/class-gutenberg-rest-block-patterns-controller.php
@@ -26,12 +26,13 @@ class Gutenberg_REST_Block_Patterns_Controller extends Gutenberg_REST_Block_Patt
*/
public function prepare_item_for_response( $item, $request ) {
$response = parent::prepare_item_for_response( $item, $request );
- if ( ! gutenberg_is_experiment_enabled( 'gutenberg-auto-inserting-blocks' ) ) {
- return $response;
- }
$data = $response->get_data();
+ if ( empty( $data['content'] ) ) {
+ return $response;
+ }
+
$blocks = parse_blocks( $data['content'] );
$data['content'] = gutenberg_serialize_blocks( $blocks ); // Serialize or render?
diff --git a/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php b/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php
index 75b3916a1697d..910f6973e53f8 100644
--- a/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php
+++ b/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php
@@ -19,7 +19,6 @@ class Gutenberg_REST_Global_Styles_Revisions_Controller_6_4 extends Gutenberg_RE
* Prepares the revision for the REST response.
*
* @since 6.3.0
- * @since 6.4.0 Added `behaviors` field to the response.
*
* @param WP_Post $post Post revision object.
* @param WP_REST_Request $request Request object.
@@ -36,7 +35,7 @@ public function prepare_item_for_response( $post, $request ) {
$fields = $this->get_fields_for_response( $request );
$data = array();
- if ( ! empty( $global_styles_config['styles'] ) || ! empty( $global_styles_config['settings'] ) || ! empty( $global_styles_config['behaviors'] ) ) {
+ if ( ! empty( $global_styles_config['styles'] ) || ! empty( $global_styles_config['settings'] ) ) {
$global_styles_config = ( new WP_Theme_JSON_Gutenberg( $global_styles_config, 'custom' ) )->get_raw_data();
if ( rest_is_field_included( 'settings', $fields ) ) {
$data['settings'] = ! empty( $global_styles_config['settings'] ) ? $global_styles_config['settings'] : new stdClass();
@@ -44,9 +43,6 @@ public function prepare_item_for_response( $post, $request ) {
if ( rest_is_field_included( 'styles', $fields ) ) {
$data['styles'] = ! empty( $global_styles_config['styles'] ) ? $global_styles_config['styles'] : new stdClass();
}
- if ( rest_is_field_included( 'behaviors', $fields ) ) {
- $data['behaviors'] = ! empty( $global_styles_config['behaviors'] ) ? $global_styles_config['behaviors'] : new stdClass();
- }
}
if ( rest_is_field_included( 'author', $fields ) ) {
@@ -88,7 +84,6 @@ public function prepare_item_for_response( $post, $request ) {
* Retrieves the revision's schema, conforming to JSON Schema.
*
* @since 6.3.0
- * @since 6.4.0 Added `behaviors` field to the schema properties.
*
* @return array Item schema data.
*/
@@ -159,11 +154,6 @@ public function get_item_schema() {
'type' => array( 'object' ),
'context' => array( 'view', 'edit' ),
),
- 'behaviors' => array(
- 'description' => __( 'Global behaviors.', 'gutenberg' ),
- 'type' => array( 'object' ),
- 'context' => array( 'view', 'edit' ),
- ),
),
);
diff --git a/lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face-resolver.php b/lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face-resolver.php
index 84375b6c52f5d..7f397663df961 100644
--- a/lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face-resolver.php
+++ b/lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face-resolver.php
@@ -5,6 +5,8 @@
* @package WordPress
* @subpackage Fonts
* @since 6.4.0
+ *
+ * @core-merge: this file is located in `wp-includes/fonts/`.
*/
if ( class_exists( 'WP_Font_Face_Resolver' ) ) {
@@ -33,7 +35,7 @@ public static function get_fonts_from_theme_json() {
$settings = gutenberg_get_global_settings();
// Bail out early if there are no font settings.
- if ( empty( $settings['typography'] ) || empty( $settings['typography']['fontFamilies'] ) ) {
+ if ( empty( $settings['typography']['fontFamilies'] ) ) {
return array();
}
diff --git a/lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face.php b/lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face.php
index 0489fe4f4fbd5..8bb1d55414854 100644
--- a/lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face.php
+++ b/lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face.php
@@ -5,6 +5,8 @@
* @package WordPress
* @subpackage Fonts
* @since 6.4.0
+ *
+ * @core-merge: this file is located in `wp-includes/fonts/`.
*/
if ( class_exists( 'WP_Font_Face' ) ) {
@@ -82,23 +84,6 @@ class WP_Font_Face {
* @since 6.4.0
*/
public function __construct() {
- /**
- * Filters the font-face property defaults.
- *
- * @since 6.4.0
- *
- * @param array $defaults {
- * An array of required font-face properties and defaults.
- *
- * @type string $provider The provider ID. Default 'local'.
- * @type string $font-family The font-family property. Default empty string.
- * @type string $font-style The font-style property. Default 'normal'.
- * @type string $font-weight The font-weight property. Default '400'.
- * @type string $font-display The font-display property. Default 'fallback'.
- * }
- */
- $this->font_face_property_defaults = apply_filters( 'wp_font_face_property_defaults', $this->font_face_property_defaults );
-
if (
function_exists( 'is_admin' ) && ! is_admin()
&&
@@ -113,7 +98,9 @@ function_exists( 'current_theme_supports' ) && ! current_theme_supports( 'html5'
*
* @since 6.4.0
*
- * @param array $fonts The fonts to generate and print @font-face styles.
+ * @param array[][] $fonts Optional. The font-families and their font variations.
+ * See {@see wp_print_font_faces()} for the supported fields.
+ * Default empty array.
*/
public function generate_and_print( array $fonts ) {
$fonts = $this->validate_fonts( $fonts );
@@ -123,10 +110,21 @@ public function generate_and_print( array $fonts ) {
return;
}
- printf(
- $this->get_style_element(),
- $this->get_css( $fonts )
- );
+ $css = $this->get_css( $fonts );
+
+ /*
+ * The font-face CSS is contained within and open a
+
+CSS;
+ $this->expectOutputString( $expected_output );
+
+ wp_print_font_faces( $fonts );
+ }
+
public function test_should_print_fonts_in_merged_data() {
switch_theme( static::FONTS_THEME );
diff --git a/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php b/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php
new file mode 100644
index 0000000000000..0682425dd6282
--- /dev/null
+++ b/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php
@@ -0,0 +1,92 @@
+setAccessible( true );
+
+ $config = array(
+ 'id' => 'my-collection',
+ 'name' => 'My Collection',
+ 'description' => 'My collection description',
+ 'data_json_file' => 'my-collection-data.json',
+ );
+ $font_collection = new WP_Font_Collection( $config );
+
+ $actual = $property->getValue( $font_collection );
+ $property->setAccessible( false );
+
+ $this->assertSame( $config, $actual );
+ }
+
+ /**
+ * @dataProvider data_should_throw_exception
+ *
+ * @param mixed $config Config of the font collection.
+ * @param string $expected_exception_message Expected exception message.
+ */
+ public function test_should_throw_exception( $config, $expected_exception_message ) {
+ $this->expectException( 'Exception' );
+ $this->expectExceptionMessage( $expected_exception_message );
+ new WP_Font_Collection( $config );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_should_throw_exception() {
+ return array(
+ 'no id' => array(
+ array(
+ 'name' => 'My Collection',
+ 'description' => 'My collection description',
+ 'data_json_file' => 'my-collection-data.json',
+ ),
+ 'Font Collection config ID is required as a non-empty string.',
+ ),
+
+ 'no config' => array(
+ '',
+ 'Font Collection config options is required as a non-empty array.',
+ ),
+
+ 'empty array' => array(
+ array(),
+ 'Font Collection config options is required as a non-empty array.',
+ ),
+
+ 'boolean instead of config array' => array(
+ false,
+ 'Font Collection config options is required as a non-empty array.',
+ ),
+
+ 'null instead of config array' => array(
+ null,
+ 'Font Collection config options is required as a non-empty array.',
+ ),
+
+ 'missing data_json_file' => array(
+ array(
+ 'id' => 'my-collection',
+ 'name' => 'My Collection',
+ 'description' => 'My collection description',
+ ),
+ 'Font Collection config "data_json_file" option is required as a non-empty string.',
+ ),
+
+ );
+ }
+}
diff --git a/phpunit/tests/fonts/font-library/wpFontCollection/getData.php b/phpunit/tests/fonts/font-library/wpFontCollection/getData.php
new file mode 100644
index 0000000000000..55d12ac1c42fd
--- /dev/null
+++ b/phpunit/tests/fonts/font-library/wpFontCollection/getData.php
@@ -0,0 +1,52 @@
+assertSame( $expected_data, $collection->get_data() );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_should_get_data() {
+ $mock_file = wp_tempnam( 'my-collection-data-' );
+ file_put_contents( $mock_file, '{"this is mock data":true}' );
+
+ return array(
+ 'with a data_json_file' => array(
+ 'config' => array(
+ 'id' => 'my-collection',
+ 'name' => 'My Collection',
+ 'description' => 'My collection description',
+ 'data_json_file' => $mock_file,
+ ),
+ 'expected_data' => array(
+ 'id' => 'my-collection',
+ 'name' => 'My Collection',
+ 'description' => 'My collection description',
+ 'data' => '{"this is mock data":true}',
+ ),
+ ),
+ );
+ }
+}
diff --git a/phpunit/tests/fonts/font-library/wpFontFamily/base.php b/phpunit/tests/fonts/font-library/wpFontFamily/base.php
index 9253885c6b3b2..3650ac7dab997 100644
--- a/phpunit/tests/fonts/font-library/wpFontFamily/base.php
+++ b/phpunit/tests/fonts/font-library/wpFontFamily/base.php
@@ -28,15 +28,15 @@ abstract class WP_Font_Family_UnitTestCase extends WP_UnitTestCase {
public static function set_up_before_class() {
parent::set_up_before_class();
- $uploads_dir = wp_upload_dir();
- static::$fonts_dir = $uploads_dir['basedir'] . '/fonts/';
+ static::$fonts_dir = WP_Font_Library::get_fonts_dir();
+ wp_mkdir_p( static::$fonts_dir );
}
public function set_up() {
parent::set_up();
$merriweather_tmp_name = wp_tempnam( 'Merriweather-' );
- file_put_contents( $merriweather_tmp_name, 'Mocking file content' );
+ copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $merriweather_tmp_name );
$this->merriweather = array(
'font_data' => array(
'name' => 'Merriweather',
@@ -60,7 +60,7 @@ public function set_up() {
'size' => 123,
),
),
- 'font_filename' => static::$fonts_dir . 'merriweather_normal_400.ttf',
+ 'font_filename' => path_join( static::$fonts_dir, 'merriweather_normal_400.ttf' ),
);
}
diff --git a/phpunit/tests/fonts/font-library/wpFontFamily/install.php b/phpunit/tests/fonts/font-library/wpFontFamily/install.php
index b067124aad9e4..ddd8c70a97972 100644
--- a/phpunit/tests/fonts/font-library/wpFontFamily/install.php
+++ b/phpunit/tests/fonts/font-library/wpFontFamily/install.php
@@ -86,16 +86,16 @@ public function data_should_not_download_when_no_fontface() {
public function test_should_download_fontfaces_and_create_post( $font_data, array $expected ) {
// Pre-checks to ensure starting conditions.
foreach ( $expected as $font_file ) {
- $font_file = static::$fonts_dir . $font_file;
- $this->assertFileDoesNotExist( $font_file, "Font file [{$font_file}] should not exist in the uploads/fonts/ directory after installing" );
+ $font_file = path_join( static::$fonts_dir, $font_file );
+ $this->assertFileDoesNotExist( $font_file, "Font file [{$font_file}] should not exist in the fonts/ directory after installing" );
}
$font = new WP_Font_Family( $font_data );
// Test.
$font->install();
foreach ( $expected as $font_file ) {
- $font_file = static::$fonts_dir . $font_file;
- $this->assertFileExists( $font_file, "Font file [{$font_file}] should exists in the uploads/fonts/ directory after installing" );
+ $font_file = path_join( static::$fonts_dir, $font_file );
+ $this->assertFileExists( $font_file, "Font file [{$font_file}] should exists in the fonts/ directory after installing" );
}
$this->assertInstanceOf( WP_Post::class, $font->get_font_post(), 'Font post should exist after install' );
}
@@ -161,15 +161,21 @@ public function data_should_download_fontfaces() {
public function test_should_move_local_fontfaces( $font_data, array $files_data, array $expected ) {
// Set up the temporary files.
foreach ( $files_data as $file ) {
- file_put_contents( $file['tmp_name'], 'Mocking file content' );
+ if ( 'font/ttf' === $file['type'] ) {
+ copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $file['tmp_name'] );
+ } elseif ( 'font/woff' === $file['type'] ) {
+ copy( __DIR__ . '/../../../data/fonts/cooper-hewitt.woff', $file['tmp_name'] );
+ } elseif ( 'font/woff2' === $file['type'] ) {
+ copy( __DIR__ . '/../../../data/fonts/DMSans.woff2', $file['tmp_name'] );
+ }
}
$font = new WP_Font_Family( $font_data );
$font->install( $files_data );
foreach ( $expected as $font_file ) {
- $font_file = static::$fonts_dir . $font_file;
- $this->assertFileExists( $font_file, "Font file [{$font_file}] should exists in the uploads/fonts/ directory after installing" );
+ $font_file = path_join( static::$fonts_dir, $font_file );
+ $this->assertFileExists( $font_file, "Font file [{$font_file}] should exists in the fonts/ directory after installing" );
}
}
@@ -180,7 +186,8 @@ public function test_should_move_local_fontfaces( $font_data, array $files_data,
*/
public function data_should_move_local_fontfaces() {
return array(
- '1 local font' => array(
+ // ttf font type.
+ '1 local font' => array(
'font_data' => array(
'name' => 'Inter',
'slug' => 'inter',
@@ -205,7 +212,7 @@ public function data_should_move_local_fontfaces() {
),
'expected' => array( 'inter_italic_900.ttf' ),
),
- '2 local fonts' => array(
+ '2 local fonts' => array(
'font_data' => array(
'name' => 'Lato',
'slug' => 'lato',
@@ -243,6 +250,285 @@ public function data_should_move_local_fontfaces() {
),
'expected' => array( 'lato_normal_400.ttf', 'lato_normal_500.ttf' ),
),
+ // woff font type.
+ 'woff local font' => array(
+ 'font_data' => array(
+ 'name' => 'Cooper Hewitt',
+ 'slug' => 'cooper-hewitt',
+ 'fontFamily' => 'Cooper Hewitt',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Cooper Hewitt',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '900',
+ 'uploadedFile' => 'files0',
+ ),
+ ),
+ ),
+ 'files_data' => array(
+ 'files0' => array(
+ 'name' => 'cooper-hewitt.woff',
+ 'type' => 'font/woff',
+ 'tmp_name' => wp_tempnam( 'Cooper-' ),
+ 'error' => 0,
+ 'size' => 123,
+ ),
+ ),
+ 'expected' => array( 'cooper-hewitt_italic_900.woff' ),
+ ),
+ // woff2 font type.
+ 'woff2 local font' => array(
+ 'font_data' => array(
+ 'name' => 'DM Sans',
+ 'slug' => 'dm-sans',
+ 'fontFamily' => 'DM Sans',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'DM Sans',
+ 'fontStyle' => 'regular',
+ 'fontWeight' => '500',
+ 'uploadedFile' => 'files0',
+ ),
+ ),
+ ),
+ 'files_data' => array(
+ 'files0' => array(
+ 'name' => 'DMSans.woff2',
+ 'type' => 'font/woff2',
+ 'tmp_name' => wp_tempnam( 'DMSans-' ),
+ 'error' => 0,
+ 'size' => 123,
+ ),
+ ),
+ 'expected' => array( 'dm-sans_regular_500.woff2' ),
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider data_should_not_install_duplicate_fontfaces
+ *
+ * @param array $font_data Font family data in theme.json format.
+ * @param array $files_data Files data in $_FILES format.
+ * @param array $expected Expected font filename(s).
+ */
+ public function test_should_not_install_duplicate_fontfaces( $font_data, array $files_data, array $expected ) {
+ // Set up the temporary files.
+ foreach ( $files_data as $file ) {
+ copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $file['tmp_name'] );
+ }
+
+ $font = new WP_Font_Family( $font_data );
+ $font->install( $files_data );
+
+ $this->assertCount( count( $expected ), $this->files_in_dir( static::$fonts_dir ), 'Font directory should contain the same number of files as expected' );
+
+ foreach ( $expected as $font_file ) {
+ $font_file = path_join( static::$fonts_dir, $font_file );
+ $this->assertFileExists( $font_file, "Font file [{$font_file}] should exists in the fonts/ directory after installing" );
+ }
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array[]
+ */
+ public function data_should_not_install_duplicate_fontfaces() {
+ return array(
+ 'single unique font face' => array(
+ 'font_data' => array(
+ 'name' => 'Inter',
+ 'slug' => 'inter',
+ 'fontFamily' => 'Inter',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Inter',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '900',
+ 'uploadedFile' => 'files0',
+ ),
+ array(
+ 'fontFamily' => 'Inter',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '900',
+ 'uploadedFile' => 'files1',
+ ),
+ ),
+ ),
+ 'files_data' => array(
+ 'files0' => array(
+ 'name' => 'inter1.ttf',
+ 'type' => 'font/ttf',
+ 'tmp_name' => wp_tempnam( 'Inter-' ),
+ 'error' => 0,
+ 'size' => 123,
+ ),
+ 'files1' => array(
+ 'name' => 'inter1.woff',
+ 'type' => 'font/woff',
+ 'tmp_name' => wp_tempnam( 'Inter-' ),
+ 'error' => 0,
+ 'size' => 123,
+ ),
+ ),
+ 'expected' => array( 'inter_italic_900.ttf' ),
+ ),
+ 'multiple unique font faces' => array(
+ 'font_data' => array(
+ 'name' => 'Lato',
+ 'slug' => 'lato',
+ 'fontFamily' => 'Lato',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Lato',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '400',
+ 'uploadedFile' => 'files0',
+ ),
+ array(
+ 'fontFamily' => 'Lato',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '500',
+ 'uploadedFile' => 'files1',
+ ),
+ array(
+ 'fontFamily' => 'Lato',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '500',
+ 'uploadedFile' => 'files2',
+ ),
+ ),
+ ),
+ 'files_data' => array(
+ 'files0' => array(
+ 'name' => 'lato1.ttf',
+ 'type' => 'font/ttf',
+ 'tmp_name' => wp_tempnam( 'Lato-' ),
+ 'error' => 0,
+ 'size' => 123,
+ ),
+ 'files1' => array(
+ 'name' => 'lato2.ttf',
+ 'type' => 'font/ttf',
+ 'tmp_name' => wp_tempnam( 'Lato-' ),
+ 'error' => 0,
+ 'size' => 123,
+ ),
+ 'files2' => array(
+ 'name' => 'lato2.woff',
+ 'type' => 'font/woff',
+ 'tmp_name' => wp_tempnam( 'Lato-' ),
+ 'error' => 0,
+ 'size' => 123,
+ ),
+ ),
+ 'expected' => array( 'lato_normal_400.ttf', 'lato_normal_500.ttf' ),
+ ),
);
}
+
+ public function test_should_overwrite_fontface_with_different_extension() {
+ $font_data_initial = array(
+ 'name' => 'Inter',
+ 'slug' => 'inter',
+ 'fontFamily' => 'Inter',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Inter',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '500',
+ 'uploadedFile' => 'files0',
+ ),
+ array(
+ 'fontFamily' => 'Inter',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '900',
+ 'uploadedFile' => 'files1',
+ ),
+ ),
+ );
+ $files_data_initial = array(
+ 'files0' => array(
+ 'name' => 'inter1.ttf',
+ 'type' => 'font/woff',
+ 'tmp_name' => wp_tempnam( 'Inter-' ),
+ 'error' => 0,
+ 'size' => 123,
+ ),
+ 'files1' => array(
+ 'name' => 'inter1.woff',
+ 'type' => 'font/woff',
+ 'tmp_name' => wp_tempnam( 'Inter-' ),
+ 'error' => 0,
+ 'size' => 123,
+ ),
+ );
+ $font_data_overwrite = array(
+ 'name' => 'Inter',
+ 'slug' => 'inter',
+ 'fontFamily' => 'Inter',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Inter',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '500',
+ 'uploadedFile' => 'files0',
+ ),
+ array(
+ 'fontFamily' => 'Inter',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '900',
+ 'uploadedFile' => 'files1',
+ ),
+ ),
+ );
+ $files_data_overwrite = array(
+ 'files0' => array(
+ 'name' => 'inter1.woff',
+ 'type' => 'font/woff',
+ 'tmp_name' => wp_tempnam( 'Inter-' ),
+ 'error' => 0,
+ 'size' => 123,
+ ),
+ 'files1' => array(
+ 'name' => 'inter1.ttf',
+ 'type' => 'font/ttf',
+ 'tmp_name' => wp_tempnam( 'Inter-' ),
+ 'error' => 0,
+ 'size' => 123,
+ ),
+ );
+
+ $expected = array( 'inter_italic_500.woff', 'inter_italic_900.ttf' );
+
+ // Set up the temporary files.
+ foreach ( $files_data_initial as $file ) {
+ if ( 'font/ttf' === $file['type'] ) {
+ copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $file['tmp_name'] );
+ } elseif ( 'font/woff' === $file['type'] ) {
+ copy( __DIR__ . '/../../../data/fonts/cooper-hewitt.woff', $file['tmp_name'] );
+ }
+ }
+ foreach ( $files_data_overwrite as $file ) {
+ if ( 'font/ttf' === $file['type'] ) {
+ copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $file['tmp_name'] );
+ } elseif ( 'font/woff' === $file['type'] ) {
+ copy( __DIR__ . '/../../../data/fonts/cooper-hewitt.woff', $file['tmp_name'] );
+ }
+ }
+
+ $font = new WP_Font_Family( $font_data_initial );
+ $font->install( $files_data_initial );
+
+ $font = new WP_Font_Family( $font_data_overwrite );
+ $font->install( $files_data_overwrite );
+
+ $this->assertCount( count( $expected ), $this->files_in_dir( static::$fonts_dir ), 'Font directory should contain the same number of files as expected' );
+
+ foreach ( $expected as $font_file ) {
+ $font_file = path_join( static::$fonts_dir, $font_file );
+ $this->assertFileExists( $font_file, "Font file [{$font_file}] should exists in the fonts/ directory after installing" );
+ }
+ }
}
diff --git a/phpunit/tests/fonts/font-library/wpFontFamily/uninstall.php b/phpunit/tests/fonts/font-library/wpFontFamily/uninstall.php
index 46ebc31a32ef9..c878dd00fdb5c 100644
--- a/phpunit/tests/fonts/font-library/wpFontFamily/uninstall.php
+++ b/phpunit/tests/fonts/font-library/wpFontFamily/uninstall.php
@@ -72,7 +72,7 @@ public function data_should_return_error_when_not_able_to_uninstall() {
public function test_should_uninstall( $font_data, array $files_data ) {
// Set up.
foreach ( $files_data as $file ) {
- file_put_contents( $file['tmp_name'], 'Mocking file content' );
+ copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $file['tmp_name'] );
}
$font = new WP_Font_Family( $font_data );
$font->install( $files_data );
@@ -103,7 +103,7 @@ public function test_should_uninstall_only_its_font_family( $font_data, array $f
// Set up the font family to be uninstalled.
foreach ( $files_data as $file ) {
- file_put_contents( $file['tmp_name'], 'Mocking file content' );
+ copy( __DIR__ . '/../../../data/fonts/Merriweather.ttf', $file['tmp_name'] );
}
$font = new WP_Font_Family( $font_data );
$font->install( $files_data );
@@ -112,7 +112,7 @@ public function test_should_uninstall_only_its_font_family( $font_data, array $f
// Check that the files were uninstalled.
foreach ( $files_to_uninstall as $font_file ) {
- $font_file = static::$fonts_dir . $font_file;
+ $font_file = path_join( static::$fonts_dir, $font_file );
$this->assertFileDoesNotExist( $font_file, "Font file [{$font_file}] should not exists in the uploads/fonts/ directory after uninstalling" );
}
// Check that the Merriweather file was not uninstalled.
diff --git a/phpunit/tests/fonts/font-library/wpFontFamilyUtils/mergeFontsData.php b/phpunit/tests/fonts/font-library/wpFontFamilyUtils/mergeFontsData.php
index b1e9bdc30aec4..21517a0970f93 100644
--- a/phpunit/tests/fonts/font-library/wpFontFamilyUtils/mergeFontsData.php
+++ b/phpunit/tests/fonts/font-library/wpFontFamilyUtils/mergeFontsData.php
@@ -76,9 +76,10 @@ public function data_should_fail_merge() {
* @param array $expected_result Expected result.
*/
public function test_should_merge( array $font1, array $font2, array $expected_result ) {
- $actual = WP_Font_Family_Utils::merge_fonts_data( $font1, $font2 );
-
- $this->assertSame( $expected_result, $actual );
+ $result = WP_Font_Family_Utils::merge_fonts_data( $font1, $font2 );
+ $this->assertSame( $expected_result, $result, 'Merged font data should match expected result.' );
+ $json_result = wp_json_encode( $result );
+ $this->assertStringContainsString( '"fontFace":[', $json_result, 'fontFace data should be enconded as an array and not an object.' );
}
/**
@@ -229,6 +230,71 @@ public function data_should_merge() {
),
),
),
+ 'repeated font faces with non consecutive index positions' => array(
+ 'font1' => array(
+ 'slug' => 'piazzolla',
+ 'name' => 'Piazzolla',
+ 'fontFamily' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '400',
+ 'src' => 'http://example.com/fonts/piazzolla_400_italic.ttf',
+ ),
+
+ ),
+ ),
+ 'font2' => array(
+ 'slug' => 'piazzolla',
+ 'fontFamily' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '600',
+ 'src' => 'http://example.com/fonts/piazzolla_600_normal.ttf',
+ ),
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '400',
+ 'src' => 'http://example.com/fonts/piazzolla_400_italic.ttf',
+ ),
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '500',
+ 'src' => 'http://example.com/fonts/piazzolla_500_italic.ttf',
+ ),
+ ),
+ ),
+ 'expected_result' => array(
+ 'slug' => 'piazzolla',
+ 'name' => 'Piazzolla',
+ 'fontFamily' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '400',
+ 'src' => 'http://example.com/fonts/piazzolla_400_italic.ttf',
+ ),
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '600',
+ 'src' => 'http://example.com/fonts/piazzolla_600_normal.ttf',
+ ),
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'italic',
+ 'fontWeight' => '500',
+ 'src' => 'http://example.com/fonts/piazzolla_500_italic.ttf',
+ ),
+ ),
+ ),
+ ),
);
}
}
diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php
new file mode 100644
index 0000000000000..b3d8d1f10a1ef
--- /dev/null
+++ b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php
@@ -0,0 +1,35 @@
+ 'my-font-collection',
+ 'name' => 'My Font Collection',
+ 'description' => 'Demo about how to a font collection to your WordPress Font Library.',
+ 'data_json_file' => path_join( __DIR__, 'my-font-collection-data.json' ),
+ );
+
+ wp_register_font_collection( $my_font_collection_config );
+ }
+
+ public function test_should_get_font_collection() {
+ $font_collection = WP_Font_Library::get_font_collection( 'my-font-collection' );
+ $this->assertInstanceOf( 'WP_Font_Collection', $font_collection );
+ }
+
+ public function test_should_get_no_font_collection_if_the_id_is_not_registered() {
+ $font_collection = WP_Font_Library::get_font_collection( 'not-registered-font-collection' );
+ $this->assertWPError( $font_collection );
+ }
+}
diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php
new file mode 100644
index 0000000000000..4bda590d6e8fd
--- /dev/null
+++ b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php
@@ -0,0 +1,45 @@
+ 'my-font-collection',
+ 'name' => 'My Font Collection',
+ 'description' => 'Demo about how to a font collection to your WordPress Font Library.',
+ 'data_json_file' => path_join( __DIR__, 'my-font-collection-data.json' ),
+ );
+
+ wp_register_font_collection( $my_font_collection_config );
+
+ $another_font_collection_config = array(
+ 'id' => 'another-font-collection',
+ 'name' => 'Another Font Collection',
+ 'description' => 'Demo about how to a font collection to your WordPress Font Library.',
+ 'data_json_file' => path_join( __DIR__, 'another-font-collection-data.json' ),
+ );
+
+ wp_register_font_collection( $another_font_collection_config );
+ }
+
+ public function test_should_get_font_collections() {
+ $font_collections = WP_Font_Library::get_font_collections();
+ $this->assertNotEmpty( $font_collections, 'Sould return an array of font collections.' );
+ $this->assertCount( 2, $font_collections, 'Should return an array with one font collection.' );
+
+ $this->assertArrayHasKey( 'my-font-collection', $font_collections, 'The array should have the key of the registered font collection id.' );
+ $this->assertInstanceOf( 'WP_Font_Collection', $font_collections['my-font-collection'], 'The value of the array $font_collections[id] should be an instance of WP_Font_Collection class.' );
+ $this->assertArrayHasKey( 'another-font-collection', $font_collections, 'The array should have the key of the registered font collection id.' );
+ $this->assertInstanceOf( 'WP_Font_Collection', $font_collections['another-font-collection'], 'The value of the array $font_collections[id] should be an instance of WP_Font_Collection class.' );
+ }
+}
diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontsDir.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontsDir.php
index 5d638696352ff..4bbafc55a2147 100644
--- a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontsDir.php
+++ b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontsDir.php
@@ -13,6 +13,6 @@
class Tests_Fonts_WpFontLibrary_GetFontsDir extends WP_UnitTestCase {
public function test_get_fonts_dir() {
- $this->assertStringEndsWith( '/wp-content/uploads/fonts', WP_Font_Library::get_fonts_dir() );
+ $this->assertStringEndsWith( '/wp-content/fonts', WP_Font_Library::get_fonts_dir() );
}
}
diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php b/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php
new file mode 100644
index 0000000000000..61b5eab873d6c
--- /dev/null
+++ b/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php
@@ -0,0 +1,77 @@
+ 'my-collection',
+ 'name' => 'My Collection',
+ 'description' => 'My Collection Description',
+ 'data_json_file' => 'my-collection-data.json',
+ );
+ $collection = WP_Font_Library::register_font_collection( $config );
+ $this->assertInstanceOf( 'WP_Font_Collection', $collection );
+ }
+
+ public function test_should_return_error_if_id_is_missing() {
+ $config = array(
+ 'name' => 'My Collection',
+ 'description' => 'My Collection Description',
+ 'data_json_file' => 'my-collection-data.json',
+ );
+ $this->expectException( 'Exception' );
+ $this->expectExceptionMessage( 'Font Collection config ID is required as a non-empty string.' );
+ WP_Font_Library::register_font_collection( $config );
+ }
+
+ public function test_should_return_error_if_name_is_missing() {
+ $config = array(
+ 'id' => 'my-collection',
+ 'description' => 'My Collection Description',
+ 'data_json_file' => 'my-collection-data.json',
+ );
+ $this->expectException( 'Exception' );
+ $this->expectExceptionMessage( 'Font Collection config name is required as a non-empty string.' );
+ WP_Font_Library::register_font_collection( $config );
+ }
+
+ public function test_should_return_error_if_config_is_empty() {
+ $config = array();
+ $this->expectException( 'Exception' );
+ $this->expectExceptionMessage( 'Font Collection config options is required as a non-empty array.' );
+ WP_Font_Library::register_font_collection( $config );
+ }
+
+ public function test_should_return_error_if_id_is_repeated() {
+ $config1 = array(
+ 'id' => 'my-collection-1',
+ 'name' => 'My Collection 1',
+ 'description' => 'My Collection 1 Description',
+ 'data_json_file' => 'my-collection-1-data.json',
+ );
+ $config2 = array(
+ 'id' => 'my-collection-1',
+ 'name' => 'My Collection 2',
+ 'description' => 'My Collection 2 Description',
+ 'data_json_file' => 'my-collection-2-data.json',
+ );
+
+ // Register first collection.
+ $collection1 = WP_Font_Library::register_font_collection( $config1 );
+ $this->assertInstanceOf( 'WP_Font_Collection', $collection1, 'A collection should be registered.' );
+
+ // Try to register a second collection with same id.
+ $collection2 = WP_Font_Library::register_font_collection( $config2 );
+ $this->assertWPError( $collection2, 'Second collection with the same id should fail.' );
+ }
+}
diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php b/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php
index 6d159d261f2f4..be15ecce89881 100644
--- a/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php
+++ b/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php
@@ -15,15 +15,15 @@ class Tests_Fonts_WpFontLibrary_SetUploadDir extends WP_UnitTestCase {
public function test_should_set_fonts_upload_dir() {
$defaults = array(
'subdir' => '/abc',
- 'basedir' => '/var/www/html/wp-content/uploads',
- 'baseurl' => 'http://example.com/wp-content/uploads',
+ 'basedir' => '/var/www/html/wp-content',
+ 'baseurl' => 'http://example.com/wp-content',
);
$expected = array(
'subdir' => '/fonts',
- 'basedir' => '/var/www/html/wp-content/uploads',
- 'baseurl' => 'http://example.com/wp-content/uploads',
- 'path' => '/var/www/html/wp-content/uploads/fonts',
- 'url' => 'http://example.com/wp-content/uploads/fonts',
+ 'basedir' => '/var/www/html/wp-content',
+ 'baseurl' => content_url(),
+ 'path' => '/var/www/html/wp-content/fonts',
+ 'url' => content_url() . '/fonts',
);
$this->assertSame( $expected, WP_Font_Library::set_upload_dir( $defaults ) );
}
diff --git a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php
new file mode 100644
index 0000000000000..f1e712bab85b4
--- /dev/null
+++ b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php
@@ -0,0 +1,34 @@
+factory->user->create(
+ array(
+ 'role' => 'administrator',
+ )
+ );
+ wp_set_current_user( $admin_id );
+ }
+
+ /**
+ * Tear down each test method.
+ */
+ public function tear_down() {
+ parent::tear_down();
+
+ // Reset $collections static property of WP_Font_Library class.
+ $reflection = new ReflectionClass( 'WP_Font_Library' );
+ $property = $reflection->getProperty( 'collections' );
+ $property->setAccessible( true );
+ $property->setValue( array() );
+ }
+}
diff --git a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php
new file mode 100644
index 0000000000000..ca661a1aac626
--- /dev/null
+++ b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php
@@ -0,0 +1,47 @@
+ 'one-collection',
+ 'name' => 'One Font Collection',
+ 'description' => 'Demo about how to a font collection to your WordPress Font Library.',
+ 'data_json_file' => $mock_file,
+ );
+ wp_register_font_collection( $config );
+ }
+
+ public function test_get_font_collection() {
+ $request = new WP_REST_Request( 'GET', '/wp/v2/fonts/collections/one-collection' );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+ $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' );
+ $this->assertArrayHasKey( 'data', $data, 'The response data does not have the key with the file data.' );
+ }
+
+ public function test_get_non_existing_collection_should_return_404() {
+ $request = new WP_REST_Request( 'GET', '/wp/v2/fonts/collections/non-existing-collection-id' );
+ $response = rest_get_server()->dispatch( $request );
+ $this->assertSame( 404, $response->get_status() );
+ }
+}
diff --git a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollections.php b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollections.php
new file mode 100644
index 0000000000000..52678aa4cb94a
--- /dev/null
+++ b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollections.php
@@ -0,0 +1,45 @@
+dispatch( $request );
+ $this->assertSame( 200, $response->get_status() );
+ $this->assertSame( array(), $response->get_data() );
+ }
+
+ public function test_get_font_collections() {
+ // Mock font collection data file.
+ $mock_file = wp_tempnam( 'my-collection-data-' );
+ file_put_contents( $mock_file, '{"this is mock data":true}' );
+
+ // Add a font collection.
+ $config = array(
+ 'id' => 'my-font-collection',
+ 'name' => 'My Font Collection',
+ 'description' => 'Demo about how to a font collection to your WordPress Font Library.',
+ 'data_json_file' => $mock_file,
+ );
+ wp_register_font_collection( $config );
+
+ $request = new WP_REST_Request( 'GET', '/wp/v2/fonts/collections' );
+ $response = rest_get_server()->dispatch( $request );
+ $data = $response->get_data();
+ $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' );
+ $this->assertCount( 1, $data, 'The response data is not an array with one element.' );
+ $this->assertArrayHasKey( 'id', $data[0], 'The response data does not have the key with the collection ID.' );
+ $this->assertArrayHasKey( 'name', $data[0], 'The response data does not have the key with the collection name.' );
+ }
+}
diff --git a/phpunit/tests/fonts/font-library/class-wp-rest-font-library-controller.php b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php
similarity index 79%
rename from phpunit/tests/fonts/font-library/class-wp-rest-font-library-controller.php
rename to phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php
index f55d221acba96..e92776b70ed64 100644
--- a/phpunit/tests/fonts/font-library/class-wp-rest-font-library-controller.php
+++ b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php
@@ -1,81 +1,27 @@
user->create(
- array(
- 'role' => 'administrator',
- )
- );
- }
-
- /**
- * @covers ::register_routes
- */
- public function test_register_routes() {
- $routes = rest_get_server()->get_routes();
- $this->assertArrayHasKey( '/wp/v2/fonts', $routes, 'Rest server has not the fonts path intialized.' );
- $this->assertCount( 2, $routes['/wp/v2/fonts'], 'Rest server has not the 2 fonts paths initialized.' );
- $this->assertArrayHasKey( 'POST', $routes['/wp/v2/fonts'][0]['methods'], 'Rest server has not the POST method for fonts intialized.' );
- $this->assertArrayHasKey( 'DELETE', $routes['/wp/v2/fonts'][1]['methods'], 'Rest server has not the DELETE method for fonts intialized.' );
- }
-
- /**
- * @covers ::uninstall_fonts
- */
- public function test_uninstall_non_existing_fonts() {
- wp_set_current_user( self::$admin_id );
- $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/fonts' );
-
- $non_existing_font_data = array(
- array(
- 'slug' => 'non-existing-font',
- 'name' => 'Non existing font',
- ),
- array(
- 'slug' => 'another-not-installed-font',
- 'name' => 'Another not installed font',
- ),
- );
-
- $uninstall_request->set_param( 'fontFamilies', $non_existing_font_data );
- $response = rest_get_server()->dispatch( $uninstall_request );
- $response->get_data();
- $this->assertSame( 500, $response->get_status(), 'The response status is not 500.' );
- }
-
+class Tests_Fonts_WPRESTFontLibraryController_InstallFonts extends WP_REST_Font_Library_Controller_UnitTestCase {
/**
- * @covers ::install_fonts
- * @covers ::uninstall_fonts
*
- * @dataProvider data_install_and_uninstall_fonts
+ * @dataProvider data_install_fonts
*
* @param array $font_families Font families to install in theme.json format.
* @param array $files Font files to install.
* @param array $expected_response Expected response data.
*/
- public function test_install_and_uninstall_fonts( $font_families, $files, $expected_response ) {
- wp_set_current_user( self::$admin_id );
+ public function test_install_fonts( $font_families, $files, $expected_response ) {
$install_request = new WP_REST_Request( 'POST', '/wp/v2/fonts' );
$font_families_json = json_encode( $font_families );
$install_request->set_param( 'fontFamilies', $font_families_json );
@@ -94,27 +40,25 @@ public function test_install_and_uninstall_fonts( $font_families, $files, $expec
if ( isset( $installed_font['fontFace'] ) || isset( $expected_font['fontFace'] ) ) {
for ( $face_index = 0; $face_index < count( $installed_font['fontFace'] ); $face_index++ ) {
// Checks that the font asset were created correctly.
- $this->assertStringEndsWith( $expected_font['fontFace'][ $face_index ]['src'], $installed_font['fontFace'][ $face_index ]['src'], 'The src of the fonts were not updated as expected.' );
+ if ( isset( $installed_font['fontFace'][ $face_index ]['src'] ) ) {
+ $this->assertStringEndsWith( $expected_font['fontFace'][ $face_index ]['src'], $installed_font['fontFace'][ $face_index ]['src'], 'The src of the fonts were not updated as expected.' );
+ }
// Removes the src from the response to compare the rest of the data.
unset( $installed_font['fontFace'][ $face_index ]['src'] );
unset( $expected_font['fontFace'][ $face_index ]['src'] );
+ unset( $installed_font['fontFace'][ $face_index ]['uploadedFile'] );
}
}
// Compares if the rest of the data is the same.
$this->assertEquals( $expected_font, $installed_font, 'The endpoint answer is not as expected.' );
}
-
- $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/fonts' );
- $uninstall_request->set_param( 'fontFamilies', $font_families );
- $response = rest_get_server()->dispatch( $uninstall_request );
- $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' );
}
/**
- * Data provider for test_install_and_uninstall_fonts
+ * Data provider for test_install_fonts
*/
- public function data_install_and_uninstall_fonts() {
+ public function data_install_fonts() {
$temp_file_path1 = wp_tempnam( 'Piazzola1-' );
file_put_contents( $temp_file_path1, 'Mocking file content' );
@@ -165,7 +109,7 @@ public function data_install_and_uninstall_fonts() {
'fontFamily' => 'Piazzolla',
'fontStyle' => 'normal',
'fontWeight' => '400',
- 'src' => '/wp-content/uploads/fonts/piazzolla_normal_400.ttf',
+ 'src' => '/wp-content/fonts/piazzolla_normal_400.ttf',
),
),
),
@@ -178,7 +122,7 @@ public function data_install_and_uninstall_fonts() {
'fontFamily' => 'Montserrat',
'fontStyle' => 'normal',
'fontWeight' => '100',
- 'src' => '/wp-content/uploads/fonts/montserrat_normal_100.ttf',
+ 'src' => '/wp-content/fonts/montserrat_normal_100.ttf',
),
),
),
@@ -319,7 +263,7 @@ public function data_install_and_uninstall_fonts() {
'fontFamily' => 'Piazzolla',
'fontStyle' => 'normal',
'fontWeight' => '400',
- 'src' => '/wp-content/uploads/fonts/piazzolla_normal_400.ttf',
+ 'src' => '/wp-content/fonts/piazzolla_normal_400.ttf',
),
),
),
@@ -332,7 +276,7 @@ public function data_install_and_uninstall_fonts() {
'fontFamily' => 'Montserrat',
'fontStyle' => 'normal',
'fontWeight' => '100',
- 'src' => '/wp-content/uploads/fonts/montserrat_normal_100.ttf',
+ 'src' => '/wp-content/fonts/montserrat_normal_100.ttf',
),
),
),
@@ -344,16 +288,12 @@ public function data_install_and_uninstall_fonts() {
/**
* Tests failure when fonfaces has improper inputs
*
- * @covers ::install_fonts
- *
* @dataProvider data_install_with_improper_inputs
*
* @param array $font_families Font families to install in theme.json format.
* @param array $files Font files to install.
*/
public function test_install_with_improper_inputs( $font_families, $files = array() ) {
- wp_set_current_user( self::$admin_id );
-
$install_request = new WP_REST_Request( 'POST', '/wp/v2/fonts' );
$font_families_json = json_encode( $font_families );
$install_request->set_param( 'fontFamilies', $font_families_json );
diff --git a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/registerRoutes.php b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/registerRoutes.php
new file mode 100644
index 0000000000000..2ac7b93c3a414
--- /dev/null
+++ b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/registerRoutes.php
@@ -0,0 +1,28 @@
+get_routes();
+ $this->assertArrayHasKey( '/wp/v2/fonts', $routes, 'Rest server has not the fonts path intialized.' );
+ $this->assertCount( 2, $routes['/wp/v2/fonts'], 'Rest server has not the 2 fonts paths initialized.' );
+ $this->assertCount( 1, $routes['/wp/v2/fonts/collections'], 'Rest server has not the collections path initialized.' );
+ $this->assertCount( 1, $routes['/wp/v2/fonts/collections/(?P[\/\w-]+)'], 'Rest server has not the collection path initialized.' );
+
+ $this->assertArrayHasKey( 'POST', $routes['/wp/v2/fonts'][0]['methods'], 'Rest server has not the POST method for fonts intialized.' );
+ $this->assertArrayHasKey( 'DELETE', $routes['/wp/v2/fonts'][1]['methods'], 'Rest server has not the DELETE method for fonts intialized.' );
+ $this->assertArrayHasKey( 'GET', $routes['/wp/v2/fonts/collections'][0]['methods'], 'Rest server has not the GET method for collections intialized.' );
+ $this->assertArrayHasKey( 'GET', $routes['/wp/v2/fonts/collections/(?P[\/\w-]+)'][0]['methods'], 'Rest server has not the GET method for collection intialized.' );
+ }
+}
diff --git a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php
new file mode 100644
index 0000000000000..3082bfc87f62e
--- /dev/null
+++ b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php
@@ -0,0 +1,97 @@
+ 'Piazzolla',
+ 'slug' => 'piazzolla',
+ 'name' => 'Piazzolla',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Piazzolla',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '400',
+ 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf',
+ 'downloadFromUrl' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf',
+ ),
+ ),
+ ),
+ array(
+ 'fontFamily' => 'Montserrat',
+ 'slug' => 'montserrat',
+ 'name' => 'Montserrat',
+ 'fontFace' => array(
+ array(
+ 'fontFamily' => 'Montserrat',
+ 'fontStyle' => 'normal',
+ 'fontWeight' => '100',
+ 'src' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf',
+ 'downloadFromUrl' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf',
+ ),
+ ),
+ ),
+ );
+
+ $install_request = new WP_REST_Request( 'POST', '/wp/v2/fonts' );
+ $font_families_json = json_encode( $mock_families );
+ $install_request->set_param( 'fontFamilies', $font_families_json );
+ rest_get_server()->dispatch( $install_request );
+ }
+
+ public function test_uninstall() {
+ $font_families_to_uninstall = array(
+ array(
+ 'slug' => 'piazzolla',
+ ),
+ array(
+ 'slug' => 'montserrat',
+ ),
+ );
+
+ $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/fonts' );
+ $uninstall_request->set_param( 'fontFamilies', $font_families_to_uninstall );
+ $response = rest_get_server()->dispatch( $uninstall_request );
+ echo ( print_r( $response->get_data(), true ) );
+ $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' );
+ }
+
+
+ public function test_uninstall_non_existing_fonts() {
+ $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/fonts' );
+
+ $non_existing_font_data = array(
+ array(
+ 'slug' => 'non-existing-font',
+ 'name' => 'Non existing font',
+ ),
+ array(
+ 'slug' => 'another-not-installed-font',
+ 'name' => 'Another not installed font',
+ ),
+ );
+
+ $uninstall_request->set_param( 'fontFamilies', $non_existing_font_data );
+ $response = rest_get_server()->dispatch( $uninstall_request );
+ $response->get_data();
+ $this->assertSame( 500, $response->get_status(), 'The response status is not 500.' );
+ }
+}
diff --git a/platform-docs/.eslintrc.js b/platform-docs/.eslintrc.js
new file mode 100644
index 0000000000000..81578e070ea50
--- /dev/null
+++ b/platform-docs/.eslintrc.js
@@ -0,0 +1,21 @@
+module.exports = {
+ root: true,
+ plugins: [ 'react' ],
+ extends: [ 'plugin:@wordpress/eslint-plugin/recommended' ],
+ settings: {
+ react: {
+ pragma: 'React',
+ version: 'detect',
+ flowVersion: '0.92.0',
+ },
+ 'import/resolver': require.resolve( '../tools/eslint/import-resolver' ),
+ },
+ overrides: [
+ {
+ files: [ '**/*.js' ],
+ rules: {
+ 'import/no-unresolved': 'off',
+ },
+ },
+ ],
+};
diff --git a/platform-docs/.gitignore b/platform-docs/.gitignore
new file mode 100644
index 0000000000000..b2d6de30624f6
--- /dev/null
+++ b/platform-docs/.gitignore
@@ -0,0 +1,20 @@
+# Dependencies
+/node_modules
+
+# Production
+/build
+
+# Generated files
+.docusaurus
+.cache-loader
+
+# Misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
diff --git a/platform-docs/README.md b/platform-docs/README.md
new file mode 100644
index 0000000000000..200a752f06e71
--- /dev/null
+++ b/platform-docs/README.md
@@ -0,0 +1,25 @@
+# Website
+
+This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
+
+### Installation
+
+```
+$ npm install
+```
+
+### Local Development
+
+```
+$ npm run start
+```
+
+This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
+
+### Build
+
+```
+$ npm run build
+```
+
+This command generates static content into the `build` directory and can be served using any static contents hosting service.
diff --git a/platform-docs/babel.config.js b/platform-docs/babel.config.js
new file mode 100644
index 0000000000000..05bbdfb96d5be
--- /dev/null
+++ b/platform-docs/babel.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ presets: [ require.resolve( '@docusaurus/core/lib/babel/preset' ) ],
+};
diff --git a/platform-docs/docs/advanced/_category_.json b/platform-docs/docs/advanced/_category_.json
new file mode 100644
index 0000000000000..a5787a8693001
--- /dev/null
+++ b/platform-docs/docs/advanced/_category_.json
@@ -0,0 +1,10 @@
+{
+ "position": 4,
+ "label": "Advanced",
+ "collapsible": true,
+ "collapsed": true,
+ "link": {
+ "type": "generated-index",
+ "title": "Advanced"
+ }
+}
diff --git a/platform-docs/docs/advanced/create-format.md b/platform-docs/docs/advanced/create-format.md
new file mode 100644
index 0000000000000..3779c5a85e5be
--- /dev/null
+++ b/platform-docs/docs/advanced/create-format.md
@@ -0,0 +1,5 @@
+---
+sidebar_position: 1
+---
+
+# Create a RichText format
\ No newline at end of file
diff --git a/platform-docs/docs/advanced/dynamic.md b/platform-docs/docs/advanced/dynamic.md
new file mode 100644
index 0000000000000..369670cb1af49
--- /dev/null
+++ b/platform-docs/docs/advanced/dynamic.md
@@ -0,0 +1,5 @@
+---
+sidebar_position: 3
+---
+
+# Augmenting blocks
\ No newline at end of file
diff --git a/platform-docs/docs/advanced/interactivity.md b/platform-docs/docs/advanced/interactivity.md
new file mode 100644
index 0000000000000..ba35f82cb2144
--- /dev/null
+++ b/platform-docs/docs/advanced/interactivity.md
@@ -0,0 +1,5 @@
+---
+sidebar_position: 2
+---
+
+# Interactivity API
\ No newline at end of file
diff --git a/platform-docs/docs/advanced/wordpress.md b/platform-docs/docs/advanced/wordpress.md
new file mode 100644
index 0000000000000..ce2e40f7c702e
--- /dev/null
+++ b/platform-docs/docs/advanced/wordpress.md
@@ -0,0 +1,5 @@
+---
+sidebar_position: 4
+---
+
+# Gutenberg and WordPress
\ No newline at end of file
diff --git a/platform-docs/docs/basic-concepts/_category_.json b/platform-docs/docs/basic-concepts/_category_.json
new file mode 100644
index 0000000000000..211a193f6937c
--- /dev/null
+++ b/platform-docs/docs/basic-concepts/_category_.json
@@ -0,0 +1,10 @@
+{
+ "position": 2,
+ "label": "Basic Concepts",
+ "collapsible": true,
+ "collapsed": false,
+ "link": {
+ "type": "generated-index",
+ "title": "Basic Concepts"
+ }
+}
diff --git a/platform-docs/docs/basic-concepts/data.md b/platform-docs/docs/basic-concepts/data.md
new file mode 100644
index 0000000000000..bc298073a3a5c
--- /dev/null
+++ b/platform-docs/docs/basic-concepts/data.md
@@ -0,0 +1,119 @@
+---
+sidebar_position: 2
+---
+
+# Data Format
+
+A block editor document is a collection of semantically consistent descriptions of what each block is and what its essential data is.
+
+The input and output of the block editor is a tree of block objects with the current format:
+
+```js
+const value = [ block1, block2, block3 ];
+```
+
+### The block object
+
+Each block object has an id, a set of attributes and potentially a list of child blocks.
+
+```js
+const block = {
+ clientId, // unique string identifier.
+ type, // The block type (paragraph, image...)
+ attributes, // (key, value) set of attributes representing the direct properties/content of the current block.
+ innerBlocks, // An array of child blocks or inner blocks.
+};
+```
+
+Note the attributes keys and types, the allowed inner blocks are defined by the block type. For example, the core quote block has a `cite` string attribute representing the cite content while a heading block has a numeric `level` attribute, representing the level of the heading (1 to 6).
+
+During the lifecycle of the block in the editor, the block object can receive extra metadata:
+
+- `isValid`: A boolean representing whether the block is valid or not;
+- `originalContent`: The original HTML serialization of the block.
+
+**Examples**
+
+```js
+// A simple paragraph block.
+const paragraphBlock1 = {
+ clientId: '51828be1-5f0d-4a6b-8099-f4c6f897e0a3',
+ type: 'core/paragraph',
+ attributes: {
+ content: 'This is the content of the paragraph block',
+ dropCap: true,
+ },
+};
+
+// A separator block.
+const separatorBlock = {
+ clientId: '51828be1-5f0d-4a6b-8099-f4c6f897e0a4',
+ type: 'core/separator',
+ attributes: {},
+};
+
+// A columns block with a paragraph block on each column.
+const columnsBlock = {
+ clientId: '51828be1-5f0d-4a6b-8099-f4c6f897e0a7',
+ type: 'core/columns',
+ attributes: {},
+ innerBlocks: [
+ {
+ clientId: '51828be1-5f0d-4a6b-8099-f4c6f897e0a5',
+ type: 'core/column',
+ attributes: {},
+ innerBlocks: [ paragraphBlock1 ],
+ },
+ {
+ clientId: '51828be1-5f0d-4a6b-8099-f4c6f897e0a6',
+ type: 'core/column',
+ attributes: {},
+ innerBlocks: [ paragraphBlock2 ],
+ },
+ ],
+};
+```
+
+## HTML serialization and parsing
+
+While block editors powered by Gutenberg manipulate and edit the content as a JavaScript array of blocks, the Gutenberg framework also offers a way to serialize the blocks into HTML and parse them back.
+
+```js
+import { serialize, parse } from '@wordpress/blocks';
+
+const value = [ block1, block2, block3 ];
+
+const html = serialize( value );
+
+const parsedValue = parse( html ); // This should be equivalent to value.
+```
+
+### Delimiters and Parsing Expression Grammar
+
+In order to keep the metadata of the blocks within the serialized HTML, we chose to use HTML comments in order to keep the formality, explicitness, and unambiguity in the existing HTML syntax.
+
+By storing data in HTML comments, we would know that we wouldn't break the rest of the HTML in the document, that browsers should ignore it, and that we could simplify our approach to parsing the document.
+
+Unique to HTML comments is the fact that they cannot legitimately exist in ambiguous places, such as inside of HTML attributes like ``. Comments are also quite permissive. Whereas HTML attributes are complicated to parse properly, comments are quite easily described by a leading ``. This simplicity and permissiveness means that the parser can be implemented in several ways without needing to understand HTML properly, and we have the liberty to use more convenient syntax inside of the comment: we only need to escape double-hyphen sequences. We take advantage of this in how we store block attributes: as JSON literals inside the comment.
+
+After running this through the parser, we're left with a simple object we can manipulate idiomatically, and we don't have to worry about escaping or unescaping the data. It's handled for us through the serialization process. Because the comments are so different from other HTML tags and because we can perform a first-pass to extract the top-level blocks, we don't actually depend on having fully valid HTML!
+
+This has dramatic implications for how simple and performant we can make our parser. These explicit boundaries also protect damage to a single block from bleeding into other blocks or tarnishing the entire document. It also allows the system to identify unrecognized blocks before rendering them.
+
+_N.B.:_ The defining aspects of blocks are their semantics and the isolation mechanism they provide: in other words, their identity. On the other hand, where their data is stored is a more liberal aspect. Blocks support more than just static local data (via JSON literals inside the HTML comment or within the block's HTML), and more mechanisms (_e.g._, global blocks or otherwise resorting to storage in complementary `WP_Post` objects) are expected. See [attributes](/docs/reference-guides/block-api/block-attributes.md) for details.
+
+### The Anatomy of a Serialized Block
+
+When blocks are serialized as HTML, their attributesādepending on the nature of the blockāare serialized to these explicit comment delimiters.
+
+```html
+
+
+
+```
+
+A purely dynamic block that is to be server-rendered before display could look like this:
+
+```html
+
+```
diff --git a/platform-docs/docs/basic-concepts/internationalization.md b/platform-docs/docs/basic-concepts/internationalization.md
new file mode 100644
index 0000000000000..3b808bd71e61b
--- /dev/null
+++ b/platform-docs/docs/basic-concepts/internationalization.md
@@ -0,0 +1,5 @@
+---
+sidebar_position: 6
+---
+
+# Internationalization
diff --git a/platform-docs/docs/basic-concepts/rendering.md b/platform-docs/docs/basic-concepts/rendering.md
new file mode 100644
index 0000000000000..f5458df2e81e7
--- /dev/null
+++ b/platform-docs/docs/basic-concepts/rendering.md
@@ -0,0 +1,5 @@
+---
+sidebar_position: 7
+---
+
+# Rendering blocks
diff --git a/platform-docs/docs/basic-concepts/rich-text.md b/platform-docs/docs/basic-concepts/rich-text.md
new file mode 100644
index 0000000000000..e3517bc93203e
--- /dev/null
+++ b/platform-docs/docs/basic-concepts/rich-text.md
@@ -0,0 +1,5 @@
+---
+sidebar_position: 5
+---
+
+# RichText and Format Library
diff --git a/platform-docs/docs/basic-concepts/settings.md b/platform-docs/docs/basic-concepts/settings.md
new file mode 100644
index 0000000000000..66d0691aef5f7
--- /dev/null
+++ b/platform-docs/docs/basic-concepts/settings.md
@@ -0,0 +1,5 @@
+---
+sidebar_position: 4
+---
+
+# Block Editor Settings
diff --git a/platform-docs/docs/basic-concepts/ui.md b/platform-docs/docs/basic-concepts/ui.md
new file mode 100644
index 0000000000000..8b6e706683d08
--- /dev/null
+++ b/platform-docs/docs/basic-concepts/ui.md
@@ -0,0 +1,29 @@
+---
+sidebar_position: 1
+---
+
+# Compose the UI
+
+## Building blocks
+
+The interface of the block editor is composed of several UI elements:
+
+ - The block list is the main area where you can interact and edit the blocks.
+ - The block toolbar: When a block is selected, the main tools to manipulate the blocks are rendered in a block toolbar. This toolbar can be rendered adjacent to the block or separate depending on the design you want to achieve for your block editor.
+ - The block inspector: Generally shown as a sidebar or a modal, the inspector shows advanced tools to manipulate the selected block.
+ - Block inserter: Can be used to select blocks to insert to your block editor.
+
+The Gutenberg platform allows you to render these pieces separately and lay them out as you wish to achieve the desired design for your interface.
+
+## The Block Toolbar
+
+Wrapping your `BlockCanvas` component within the `BlockTools` wrapper allows the editor to render a block toolbar adjacent to the selected block.
+
+## The Block Inspector
+
+You can use the `BlockInspector` to render what is called the block inspector. It's a set of tools that are specific to the selected block.
+While the block toolbar contains what are considered to be the "main tools" to manipulate each block, the block inspector is meant to render advanced tools and customization options. It is generally rendered as a sidebar or a modal.
+
+## The Block Inserter
+
+By default the block editor renders a block inserter at the end of the canvas if there's no block selected. It also renders an inserter if you hover over the area between two consecutive blocks. That said, you can also decide to render a permanent inserter, for instance in a header of your editor. You can do so by using the `Inserter` component.
diff --git a/platform-docs/docs/basic-concepts/undo-redo.md b/platform-docs/docs/basic-concepts/undo-redo.md
new file mode 100644
index 0000000000000..26981e6e0f5c1
--- /dev/null
+++ b/platform-docs/docs/basic-concepts/undo-redo.md
@@ -0,0 +1,67 @@
+---
+sidebar_position: 3
+---
+
+# Undo Redo
+
+## Undo/Redo using `useStateWithHistory`
+
+By default, the block editor doesn't enable undo/redo. This is because in most scenarios, the block editor is rendered as part of a larger application that already has its own undo/redo functionality. However, to help you implement undo/redo in your application, the block editor provides a set of APIs that you can use.
+
+The simplest approach is to rely on the `useStateWithHistory` hook provided by the `@wordpress/compose` package. This hook is a wrapper around the `useState` hook that adds undo/redo functionality to the state.
+
+First, make sure you add the `@wordpress/compose` package to your dependencies, then use the hook like so:
+
+```jsx
+import { useStateWithHistory } from '@wordpress/compose';
+import { createRoot, createElement, useState } from "@wordpress/element";
+import {
+ BlockEditorProvider,
+ BlockCanvas,
+} from "@wordpress/block-editor";
+
+function Editor() {
+ const { value, setValue, hasUndo, hasRedo, undo, redo } =
+ useStateWithHistory( { blocks: [] } );
+
+ return (
+
+ setValue( { blocks, selection }, true )
+ }
+ onChange={ ( blocks, { selection } ) =>
+ setValue( { blocks, selection }, false )
+ }
+ >
+
+
+
+
+
+
+ );
+}
+```
+
+The `useStateWithHistory` hook returns an object with the following properties:
+
+- `value`: the current value of the state.
+- `setValue`: a function that can be used to update the state.
+- `hasUndo`: a boolean indicating whether there are any actions that can be undone.
+- `hasRedo`: a boolean indicating whether there are any actions that can be redone.
+- `undo`: a function that can be used to undo the last action.
+- `redo`: a function that can be used to redo the last action.
+
+Notice that in addition to the `blocks` property, the `value` object also tracks a `selection` property. This property is used to store and control the cursor position within the block editor. This allows the editor to restore the right position when undoing or redoing changes.
+
+## Going Further...
+
+Oftentimes, editors allow to track changes across multiple objects and properties. For instance, a basic writing experience might allow editing the title of a post in a normal input and the content of the post in the block editor. Or your editor might allow edits to related objects like categories, tags, etc... In these cases, you might want to implement undo/redo functionality that can be used to track changes across all of these objects and properties.
+
+The `useStateWithHistory` might not always be the right approach in these situations. Consider checking the `@wordpress/undo-manager` package that offers a lower level undo manager that can be adapted more easily to your specific needs.
diff --git a/platform-docs/docs/create-block/_category_.json b/platform-docs/docs/create-block/_category_.json
new file mode 100644
index 0000000000000..13d354a69f303
--- /dev/null
+++ b/platform-docs/docs/create-block/_category_.json
@@ -0,0 +1,10 @@
+{
+ "position": 3,
+ "label": "Create a block type",
+ "collapsible": true,
+ "collapsed": true,
+ "link": {
+ "type": "generated-index",
+ "title": "Create a block type"
+ }
+}
diff --git a/platform-docs/docs/create-block/attributes.md b/platform-docs/docs/create-block/attributes.md
new file mode 100644
index 0000000000000..044b326ae54a5
--- /dev/null
+++ b/platform-docs/docs/create-block/attributes.md
@@ -0,0 +1,5 @@
+---
+sidebar_position: 4
+---
+
+# Block attributes
\ No newline at end of file
diff --git a/platform-docs/docs/create-block/block-supports.md b/platform-docs/docs/create-block/block-supports.md
new file mode 100644
index 0000000000000..20cfccebd4b05
--- /dev/null
+++ b/platform-docs/docs/create-block/block-supports.md
@@ -0,0 +1,3 @@
+---
+sidebar_position: 3
+---
diff --git a/platform-docs/docs/create-block/first-block-type.md b/platform-docs/docs/create-block/first-block-type.md
new file mode 100644
index 0000000000000..26925aab51987
--- /dev/null
+++ b/platform-docs/docs/create-block/first-block-type.md
@@ -0,0 +1,5 @@
+---
+sidebar_position: 1
+---
+
+# Your first block type
\ No newline at end of file
diff --git a/platform-docs/docs/create-block/nested-blocks.md b/platform-docs/docs/create-block/nested-blocks.md
new file mode 100644
index 0000000000000..78352ba3ebe43
--- /dev/null
+++ b/platform-docs/docs/create-block/nested-blocks.md
@@ -0,0 +1,5 @@
+---
+sidebar_position: 5
+---
+
+# Nested blocks
\ No newline at end of file
diff --git a/platform-docs/docs/create-block/transforms.md b/platform-docs/docs/create-block/transforms.md
new file mode 100644
index 0000000000000..ab631d44e1420
--- /dev/null
+++ b/platform-docs/docs/create-block/transforms.md
@@ -0,0 +1,5 @@
+---
+sidebar_position: 6
+---
+
+# Block Transforms
\ No newline at end of file
diff --git a/platform-docs/docs/create-block/using-styles.md b/platform-docs/docs/create-block/using-styles.md
new file mode 100644
index 0000000000000..1d5ac2bcd0721
--- /dev/null
+++ b/platform-docs/docs/create-block/using-styles.md
@@ -0,0 +1,5 @@
+---
+sidebar_position: 2
+---
+
+# Using styles and stylesheets
\ No newline at end of file
diff --git a/platform-docs/docs/create-block/writing-flow.md b/platform-docs/docs/create-block/writing-flow.md
new file mode 100644
index 0000000000000..187d39ba35c5e
--- /dev/null
+++ b/platform-docs/docs/create-block/writing-flow.md
@@ -0,0 +1,5 @@
+---
+sidebar_position: 7
+---
+
+# Writing Flow and Pasting
\ No newline at end of file
diff --git a/platform-docs/docs/intro.md b/platform-docs/docs/intro.md
new file mode 100644
index 0000000000000..41732843164e2
--- /dev/null
+++ b/platform-docs/docs/intro.md
@@ -0,0 +1,121 @@
+---
+sidebar_position: 1
+---
+
+# Getting Started
+
+Let's discover how to use the **Gutenberg Block Editor** to build your own block editor in less than 10 minutes**.
+
+
+## What you'll need
+
+- [Node.js](https://nodejs.org/en/download/) version 16.14 or above.
+- We're going to be using "vite" to setup our single page application (SPA) that contains a block editor. You can use your own setup, and your own application for this.
+
+## Preparing the SPA powered by Vite.
+
+First bootstrap a vite project using `npm create vite@latest` and pick `Vanilla` variant and `JavaScript` as a language.
+
+Once done, you can navigate to your application folder and run it locally using `npm run dev`. Open the displayed local URL in a browser.
+
+## Installing dependencies
+
+To build a block editor, you need to install the following dependencies:
+
+ - `@wordpress/block-editor`
+ - `@wordpress/element`
+ - `@wordpress/block-library`
+ - `@wordpress/components`
+
+## Setup vite to use JSX and @wordpress/element as a pragma
+
+We're going to be using JSX to write our UI and components. So one of the first steps we need to do is to configure our build tooling.
+
+If you're using vite, you can create a `vite.config.js` file at the root of your application and paste the following content:
+
+```js
+// vite.config.js
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ esbuild: {
+ jsxFactory: 'createElement',
+ jsxFragment: 'Fragment',
+ },
+ define: {
+ // This is not necessary to make JSX work
+ // but it's a requirement for some @wordpress packages.
+ 'process.env': {}
+ },
+})
+```
+
+With the config above, you should be able to write JSX in your `*.jsx` files, just make sure you import createElement and Fragment at the top of each of your files: `import { createElement, Fragment } from '@wordpress/element';`
+
+We can now check that everything is working as expected:
+
+ - Remove the default content created by vite: `main.js` and `counter.js` files.
+ - Create an `index.jsx` file at the root of your application containing some JSX, example:
+```jsx
+import { createRoot, createElement } from '@wordpress/element';
+
+// Render your React component instead
+const root = createRoot(document.getElementById('app'));
+root.render(
"`;
exports[`Blocks raw handling pasteHandler gutenberg 1`] = `"Test"`;
@@ -174,10 +174,10 @@ exports[`rawHandler should convert a caption shortcode with link 1`] = `
`;
exports[`rawHandler should convert a list with attributes 1`] = `
-"
-
-